commit eb32dbd5b252ba12b8e2170d96c3f51f8176c9de
Author: Vince <vi.teissier@gmail.com>
Date:   Tue May 28 16:10:53 2024 +0200

    Initial commit

diff --git a/application/.htaccess b/application/.htaccess
new file mode 100644
index 0000000..6c63ed4
--- /dev/null
+++ b/application/.htaccess
@@ -0,0 +1,6 @@
+<IfModule authz_core_module>
+    Require all denied
+</IfModule>
+<IfModule !authz_core_module>
+    Deny from all
+</IfModule>
\ No newline at end of file
diff --git a/application/cache/index.html b/application/cache/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/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/application/config/autoload.php b/application/config/autoload.php
new file mode 100644
index 0000000..7cdc901
--- /dev/null
+++ b/application/config/autoload.php
@@ -0,0 +1,135 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/*
+| -------------------------------------------------------------------
+| AUTO-LOADER
+| -------------------------------------------------------------------
+| This file specifies which systems should be loaded by default.
+|
+| In order to keep the framework as light-weight as possible only the
+| absolute minimal resources are loaded by default. For example,
+| the database is not connected to automatically since no assumption
+| is made regarding whether you intend to use it.  This file lets
+| you globally define which systems you would like loaded with every
+| request.
+|
+| -------------------------------------------------------------------
+| Instructions
+| -------------------------------------------------------------------
+|
+| These are the things you can load automatically:
+|
+| 1. Packages
+| 2. Libraries
+| 3. Drivers
+| 4. Helper files
+| 5. Custom config files
+| 6. Language files
+| 7. Models
+|
+*/
+
+/*
+| -------------------------------------------------------------------
+|  Auto-load Packages
+| -------------------------------------------------------------------
+| Prototype:
+|
+|  $autoload['packages'] = array(APPPATH.'third_party', '/usr/local/shared');
+|
+*/
+$autoload['packages'] = array();
+
+/*
+| -------------------------------------------------------------------
+|  Auto-load Libraries
+| -------------------------------------------------------------------
+| These are the classes located in system/libraries/ or your
+| application/libraries/ directory, with the addition of the
+| 'database' library, which is somewhat of a special case.
+|
+| Prototype:
+|
+|	$autoload['libraries'] = array('database', 'email', 'session');
+|
+| You can also supply an alternative library name to be assigned
+| in the controller:
+|
+|	$autoload['libraries'] = array('user_agent' => 'ua');
+*/
+$autoload['libraries'] = array();
+
+/*
+| -------------------------------------------------------------------
+|  Auto-load Drivers
+| -------------------------------------------------------------------
+| These classes are located in system/libraries/ or in your
+| application/libraries/ directory, but are also placed inside their
+| own subdirectory and they extend the CI_Driver_Library class. They
+| offer multiple interchangeable driver options.
+|
+| Prototype:
+|
+|	$autoload['drivers'] = array('cache');
+|
+| You can also supply an alternative property name to be assigned in
+| the controller:
+|
+|	$autoload['drivers'] = array('cache' => 'cch');
+|
+*/
+$autoload['drivers'] = array();
+
+/*
+| -------------------------------------------------------------------
+|  Auto-load Helper Files
+| -------------------------------------------------------------------
+| Prototype:
+|
+|	$autoload['helper'] = array('url', 'file');
+*/
+$autoload['helper'] = array();
+
+/*
+| -------------------------------------------------------------------
+|  Auto-load Config files
+| -------------------------------------------------------------------
+| Prototype:
+|
+|	$autoload['config'] = array('config1', 'config2');
+|
+| NOTE: This item is intended for use ONLY if you have created custom
+| config files.  Otherwise, leave it blank.
+|
+*/
+$autoload['config'] = array();
+
+/*
+| -------------------------------------------------------------------
+|  Auto-load Language files
+| -------------------------------------------------------------------
+| Prototype:
+|
+|	$autoload['language'] = array('lang1', 'lang2');
+|
+| NOTE: Do not include the "_lang" part of your file.  For example
+| "codeigniter_lang.php" would be referenced as array('codeigniter');
+|
+*/
+$autoload['language'] = array();
+
+/*
+| -------------------------------------------------------------------
+|  Auto-load Models
+| -------------------------------------------------------------------
+| Prototype:
+|
+|	$autoload['model'] = array('first_model', 'second_model');
+|
+| You can also supply an alternative model name to be assigned
+| in the controller:
+|
+|	$autoload['model'] = array('first_model' => 'first');
+*/
+$autoload['model'] = array();
diff --git a/application/config/config.php b/application/config/config.php
new file mode 100644
index 0000000..35ace5c
--- /dev/null
+++ b/application/config/config.php
@@ -0,0 +1,532 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/*
+|--------------------------------------------------------------------------
+| Base Site URL
+|--------------------------------------------------------------------------
+|
+| URL to your CodeIgniter root. Typically this will be your base URL,
+| WITH a trailing slash:
+|
+|	http://example.com/
+|
+| WARNING: You MUST set this value!
+|
+| If it is not set, then CodeIgniter will try to guess the protocol and
+| path to your installation, but due to security concerns the hostname will
+| be set to $_SERVER['SERVER_ADDR'] if available, or localhost otherwise.
+| The auto-detection mechanism exists only for convenience during
+| development and MUST NOT be used in production!
+|
+| If you need to allow multiple domains, remember that this file is still
+| a PHP script and you can easily do that on your own.
+|
+*/
+$config['base_url'] = '';
+
+/*
+|--------------------------------------------------------------------------
+| Index File
+|--------------------------------------------------------------------------
+|
+| Typically this will be your index.php file, unless you've renamed it to
+| something else. If you are using mod_rewrite to remove the page set this
+| variable so that it is blank.
+|
+*/
+$config['index_page'] = 'index.php';
+
+/*
+|--------------------------------------------------------------------------
+| URI PROTOCOL
+|--------------------------------------------------------------------------
+|
+| This item determines which server global should be used to retrieve the
+| URI string.  The default setting of 'REQUEST_URI' works for most servers.
+| If your links do not seem to work, try one of the other delicious flavors:
+|
+| 'REQUEST_URI'    Uses $_SERVER['REQUEST_URI']
+| 'QUERY_STRING'   Uses $_SERVER['QUERY_STRING']
+| 'PATH_INFO'      Uses $_SERVER['PATH_INFO']
+|
+| WARNING: If you set this to 'PATH_INFO', URIs will always be URL-decoded!
+*/
+$config['uri_protocol']	= 'REQUEST_URI';
+
+/*
+|--------------------------------------------------------------------------
+| URL suffix
+|--------------------------------------------------------------------------
+|
+| This option allows you to add a suffix to all URLs generated by CodeIgniter.
+| For more information please see the user guide:
+|
+| https://codeigniter.com/userguide3/general/urls.html
+|
+| Note: This option is ignored for CLI requests.
+*/
+$config['url_suffix'] = '';
+
+/*
+|--------------------------------------------------------------------------
+| Default Language
+|--------------------------------------------------------------------------
+|
+| This determines which set of language files should be used. Make sure
+| there is an available translation if you intend to use something other
+| than english.
+|
+*/
+$config['language']	= 'english';
+
+/*
+|--------------------------------------------------------------------------
+| Default Character Set
+|--------------------------------------------------------------------------
+|
+| This determines which character set is used by default in various methods
+| that require a character set to be provided.
+|
+| See http://php.net/htmlspecialchars for a list of supported charsets.
+|
+*/
+$config['charset'] = 'UTF-8';
+
+/*
+|--------------------------------------------------------------------------
+| Enable/Disable System Hooks
+|--------------------------------------------------------------------------
+|
+| If you would like to use the 'hooks' feature you must enable it by
+| setting this variable to TRUE (boolean).  See the user guide for details.
+|
+*/
+$config['enable_hooks'] = FALSE;
+
+/*
+|--------------------------------------------------------------------------
+| Class Extension Prefix
+|--------------------------------------------------------------------------
+|
+| This item allows you to set the filename/classname prefix when extending
+| native libraries.  For more information please see the user guide:
+|
+| https://codeigniter.com/userguide3/general/core_classes.html
+| https://codeigniter.com/userguide3/general/creating_libraries.html
+|
+*/
+$config['subclass_prefix'] = 'MY_';
+
+/*
+|--------------------------------------------------------------------------
+| Composer auto-loading
+|--------------------------------------------------------------------------
+|
+| Enabling this setting will tell CodeIgniter to look for a Composer
+| package auto-loader script in application/vendor/autoload.php.
+|
+|	$config['composer_autoload'] = TRUE;
+|
+| Or if you have your vendor/ directory located somewhere else, you
+| can opt to set a specific path as well:
+|
+|	$config['composer_autoload'] = '/path/to/vendor/autoload.php';
+|
+| For more information about Composer, please visit http://getcomposer.org/
+|
+| Note: This will NOT disable or override the CodeIgniter-specific
+|	autoloading (application/config/autoload.php)
+*/
+$config['composer_autoload'] = FALSE;
+
+/*
+|--------------------------------------------------------------------------
+| Allowed URL Characters
+|--------------------------------------------------------------------------
+|
+| This lets you specify which characters are permitted within your URLs.
+| When someone tries to submit a URL with disallowed characters they will
+| get a warning message.
+|
+| As a security measure you are STRONGLY encouraged to restrict URLs to
+| as few characters as possible.  By default only these are allowed: a-z 0-9~%.:_-
+|
+| Leave blank to allow all characters -- but only if you are insane.
+|
+| The configured value is actually a regular expression character group
+| and it will be executed as: ! preg_match('/^[<permitted_uri_chars>]+$/i
+|
+| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
+|
+*/
+$config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-';
+
+/*
+|--------------------------------------------------------------------------
+| Enable Query Strings
+|--------------------------------------------------------------------------
+|
+| By default CodeIgniter uses search-engine friendly segment based URLs:
+| example.com/who/what/where/
+|
+| You can optionally enable standard query string based URLs:
+| example.com?who=me&what=something&where=here
+|
+| Options are: TRUE or FALSE (boolean)
+|
+| The other items let you set the query string 'words' that will
+| invoke your controllers and its functions:
+| example.com/index.php?c=controller&m=function
+|
+| Please note that some of the helpers won't work as expected when
+| this feature is enabled, since CodeIgniter is designed primarily to
+| use segment based URLs.
+|
+*/
+$config['enable_query_strings'] = FALSE;
+$config['controller_trigger'] = 'c';
+$config['function_trigger'] = 'm';
+$config['directory_trigger'] = 'd';
+
+/*
+|--------------------------------------------------------------------------
+| Allow $_GET array
+|--------------------------------------------------------------------------
+|
+| By default CodeIgniter enables access to the $_GET array.  If for some
+| reason you would like to disable it, set 'allow_get_array' to FALSE.
+|
+| WARNING: This feature is DEPRECATED and currently available only
+|          for backwards compatibility purposes!
+|
+*/
+$config['allow_get_array'] = TRUE;
+
+/*
+|--------------------------------------------------------------------------
+| Error Logging Threshold
+|--------------------------------------------------------------------------
+|
+| You can enable error logging by setting a threshold over zero. The
+| threshold determines what gets logged. Threshold options are:
+|
+|	0 = Disables logging, Error logging TURNED OFF
+|	1 = Error Messages (including PHP errors)
+|	2 = Debug Messages
+|	3 = Informational Messages
+|	4 = All Messages
+|
+| You can also pass an array with threshold levels to show individual error types
+|
+| 	array(2) = Debug Messages, without Error Messages
+|
+| For a live site you'll usually only enable Errors (1) to be logged otherwise
+| your log files will fill up very fast.
+|
+*/
+$config['log_threshold'] = 0;
+
+/*
+|--------------------------------------------------------------------------
+| Error Logging Directory Path
+|--------------------------------------------------------------------------
+|
+| Leave this BLANK unless you would like to set something other than the default
+| application/logs/ directory. Use a full server path with trailing slash.
+|
+*/
+$config['log_path'] = '';
+
+/*
+|--------------------------------------------------------------------------
+| Log File Extension
+|--------------------------------------------------------------------------
+|
+| The default filename extension for log files. The default 'php' allows for
+| protecting the log files via basic scripting, when they are to be stored
+| under a publicly accessible directory.
+|
+| Note: Leaving it blank will default to 'php'.
+|
+*/
+$config['log_file_extension'] = '';
+
+/*
+|--------------------------------------------------------------------------
+| Log File Permissions
+|--------------------------------------------------------------------------
+|
+| The file system permissions to be applied on newly created log files.
+|
+| IMPORTANT: This MUST be an integer (no quotes) and you MUST use octal
+|            integer notation (i.e. 0700, 0644, etc.)
+*/
+$config['log_file_permissions'] = 0644;
+
+/*
+|--------------------------------------------------------------------------
+| Date Format for Logs
+|--------------------------------------------------------------------------
+|
+| Each item that is logged has an associated date. You can use PHP date
+| codes to set your own date formatting
+|
+*/
+$config['log_date_format'] = 'Y-m-d H:i:s';
+
+/*
+|--------------------------------------------------------------------------
+| Error Views Directory Path
+|--------------------------------------------------------------------------
+|
+| Leave this BLANK unless you would like to set something other than the default
+| application/views/errors/ directory.  Use a full server path with trailing slash.
+|
+*/
+$config['error_views_path'] = '';
+
+/*
+|--------------------------------------------------------------------------
+| Cache Directory Path
+|--------------------------------------------------------------------------
+|
+| Leave this BLANK unless you would like to set something other than the default
+| application/cache/ directory.  Use a full server path with trailing slash.
+|
+*/
+$config['cache_path'] = '';
+
+/*
+|--------------------------------------------------------------------------
+| Cache Include Query String
+|--------------------------------------------------------------------------
+|
+| Whether to take the URL query string into consideration when generating
+| output cache files. Valid options are:
+|
+|	FALSE      = Disabled
+|	TRUE       = Enabled, take all query parameters into account.
+|	             Please be aware that this may result in numerous cache
+|	             files generated for the same page over and over again.
+|	array('q') = Enabled, but only take into account the specified list
+|	             of query parameters.
+|
+*/
+$config['cache_query_string'] = FALSE;
+
+/*
+|--------------------------------------------------------------------------
+| Encryption Key
+|--------------------------------------------------------------------------
+|
+| If you use the Encryption class, you must set an encryption key.
+| See the user guide for more info.
+|
+| https://codeigniter.com/userguide3/libraries/encryption.html
+|
+*/
+$config['encryption_key'] = '';
+
+/*
+|--------------------------------------------------------------------------
+| Session Variables
+|--------------------------------------------------------------------------
+|
+| 'sess_driver'
+|
+|	The storage driver to use: files, database, redis, memcached
+|
+| 'sess_cookie_name'
+|
+|	The session cookie name, must contain only [0-9a-z_-] characters
+|
+| 'sess_samesite'
+|
+|	Session cookie SameSite attribute: Lax (default), Strict or None
+|
+| 'sess_expiration'
+|
+|	The number of SECONDS you want the session to last.
+|	Setting to 0 (zero) means expire when the browser is closed.
+|
+| 'sess_save_path'
+|
+|	The location to save sessions to, driver dependent.
+|
+|	For the 'files' driver, it's a path to a writable directory.
+|	WARNING: Only absolute paths are supported!
+|
+|	For the 'database' driver, it's a table name.
+|	Please read up the manual for the format with other session drivers.
+|
+|	IMPORTANT: You are REQUIRED to set a valid save path!
+|
+| 'sess_match_ip'
+|
+|	Whether to match the user's IP address when reading the session data.
+|
+|	WARNING: If you're using the database driver, don't forget to update
+|	         your session table's PRIMARY KEY when changing this setting.
+|
+| 'sess_time_to_update'
+|
+|	How many seconds between CI regenerating the session ID.
+|
+| 'sess_regenerate_destroy'
+|
+|	Whether to destroy session data associated with the old session ID
+|	when auto-regenerating the session ID. When set to FALSE, the data
+|	will be later deleted by the garbage collector.
+|
+| Other session cookie settings are shared with the rest of the application,
+| except for 'cookie_prefix' and 'cookie_httponly', which are ignored here.
+|
+*/
+$config['sess_driver'] = 'files';
+$config['sess_cookie_name'] = 'ci_session';
+$config['sess_samesite'] = 'Lax';
+$config['sess_expiration'] = 7200;
+$config['sess_save_path'] = NULL;
+$config['sess_match_ip'] = FALSE;
+$config['sess_time_to_update'] = 300;
+$config['sess_regenerate_destroy'] = FALSE;
+
+/*
+|--------------------------------------------------------------------------
+| Cookie Related Variables
+|--------------------------------------------------------------------------
+|
+| 'cookie_prefix'   = Set a cookie name prefix if you need to avoid collisions
+| 'cookie_domain'   = Set to .your-domain.com for site-wide cookies
+| 'cookie_path'     = Typically will be a forward slash
+| 'cookie_secure'   = Cookie will only be set if a secure HTTPS connection exists.
+| 'cookie_httponly' = Cookie will only be accessible via HTTP(S) (no javascript)
+| 'cookie_samesite' = Cookie's samesite attribute (Lax, Strict or None)
+|
+| Note: These settings (with the exception of 'cookie_prefix' and
+|       'cookie_httponly') will also affect sessions.
+|
+*/
+$config['cookie_prefix']	= '';
+$config['cookie_domain']	= '';
+$config['cookie_path']		= '/';
+$config['cookie_secure']	= FALSE;
+$config['cookie_httponly'] 	= FALSE;
+$config['cookie_samesite'] 	= 'Lax';
+
+/*
+|--------------------------------------------------------------------------
+| Standardize newlines
+|--------------------------------------------------------------------------
+|
+| Determines whether to standardize newline characters in input data,
+| meaning to replace \r\n, \r, \n occurrences with the PHP_EOL value.
+|
+| WARNING: This feature is DEPRECATED and currently available only
+|          for backwards compatibility purposes!
+|
+*/
+$config['standardize_newlines'] = FALSE;
+
+/*
+|--------------------------------------------------------------------------
+| Global XSS Filtering
+|--------------------------------------------------------------------------
+|
+| Determines whether the XSS filter is always active when GET, POST or
+| COOKIE data is encountered
+|
+| WARNING: This feature is DEPRECATED and currently available only
+|          for backwards compatibility purposes!
+|
+*/
+$config['global_xss_filtering'] = FALSE;
+
+/*
+|--------------------------------------------------------------------------
+| Cross Site Request Forgery
+|--------------------------------------------------------------------------
+| Enables a CSRF cookie token to be set. When set to TRUE, token will be
+| checked on a submitted form. If you are accepting user data, it is strongly
+| recommended CSRF protection be enabled.
+|
+| 'csrf_token_name' = The token name
+| 'csrf_cookie_name' = The cookie name
+| 'csrf_expire' = The number in seconds the token should expire.
+| 'csrf_regenerate' = Regenerate token on every submission
+| 'csrf_exclude_uris' = Array of URIs which ignore CSRF checks
+*/
+$config['csrf_protection'] = FALSE;
+$config['csrf_token_name'] = 'csrf_test_name';
+$config['csrf_cookie_name'] = 'csrf_cookie_name';
+$config['csrf_expire'] = 7200;
+$config['csrf_regenerate'] = TRUE;
+$config['csrf_exclude_uris'] = array();
+
+/*
+|--------------------------------------------------------------------------
+| Output Compression
+|--------------------------------------------------------------------------
+|
+| Enables Gzip output compression for faster page loads.  When enabled,
+| the output class will test whether your server supports Gzip.
+| Even if it does, however, not all browsers support compression
+| so enable only if you are reasonably sure your visitors can handle it.
+|
+| Only used if zlib.output_compression is turned off in your php.ini.
+| Please do not use it together with httpd-level output compression.
+|
+| VERY IMPORTANT:  If you are getting a blank page when compression is enabled it
+| means you are prematurely outputting something to your browser. It could
+| even be a line of whitespace at the end of one of your scripts.  For
+| compression to work, nothing can be sent before the output buffer is called
+| by the output class.  Do not 'echo' any values with compression enabled.
+|
+*/
+$config['compress_output'] = FALSE;
+
+/*
+|--------------------------------------------------------------------------
+| Master Time Reference
+|--------------------------------------------------------------------------
+|
+| Options are 'local' or any PHP supported timezone. This preference tells
+| the system whether to use your server's local time as the master 'now'
+| reference, or convert it to the configured one timezone. See the 'date
+| helper' page of the user guide for information regarding date handling.
+|
+*/
+$config['time_reference'] = 'local';
+
+/*
+|--------------------------------------------------------------------------
+| Rewrite PHP Short Tags
+|--------------------------------------------------------------------------
+|
+| If your PHP installation does not have short tag support enabled CI
+| can rewrite the tags on-the-fly, enabling you to utilize that syntax
+| in your view files.  Options are TRUE or FALSE (boolean)
+|
+| Note: You need to have eval() enabled for this to work.
+|
+*/
+$config['rewrite_short_tags'] = FALSE;
+
+/*
+|--------------------------------------------------------------------------
+| Reverse Proxy IPs
+|--------------------------------------------------------------------------
+|
+| If your server is behind a reverse proxy, you must whitelist the proxy
+| IP addresses from which CodeIgniter should trust headers such as
+| HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP in order to properly identify
+| the visitor's IP address.
+|
+| You can use both an array or a comma-separated list of proxy addresses,
+| as well as specifying whole subnets. Here are a few examples:
+|
+| Comma-separated:	'10.0.1.200,192.168.5.0/24'
+| Array:		array('10.0.1.200', '192.168.5.0/24')
+*/
+$config['proxy_ips'] = '';
diff --git a/application/config/constants.php b/application/config/constants.php
new file mode 100644
index 0000000..18d3b4b
--- /dev/null
+++ b/application/config/constants.php
@@ -0,0 +1,85 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/*
+|--------------------------------------------------------------------------
+| Display Debug backtrace
+|--------------------------------------------------------------------------
+|
+| If set to TRUE, a backtrace will be displayed along with php errors. If
+| error_reporting is disabled, the backtrace will not display, regardless
+| of this setting
+|
+*/
+defined('SHOW_DEBUG_BACKTRACE') OR define('SHOW_DEBUG_BACKTRACE', TRUE);
+
+/*
+|--------------------------------------------------------------------------
+| File and Directory Modes
+|--------------------------------------------------------------------------
+|
+| These prefs are used when checking and setting modes when working
+| with the file system.  The defaults are fine on servers with proper
+| security, but you may wish (or even need) to change the values in
+| certain environments (Apache running a separate process for each
+| user, PHP under CGI with Apache suEXEC, etc.).  Octal values should
+| always be used to set the mode correctly.
+|
+*/
+defined('FILE_READ_MODE')  OR define('FILE_READ_MODE', 0644);
+defined('FILE_WRITE_MODE') OR define('FILE_WRITE_MODE', 0666);
+defined('DIR_READ_MODE')   OR define('DIR_READ_MODE', 0755);
+defined('DIR_WRITE_MODE')  OR define('DIR_WRITE_MODE', 0755);
+
+/*
+|--------------------------------------------------------------------------
+| File Stream Modes
+|--------------------------------------------------------------------------
+|
+| These modes are used when working with fopen()/popen()
+|
+*/
+defined('FOPEN_READ')                           OR define('FOPEN_READ', 'rb');
+defined('FOPEN_READ_WRITE')                     OR define('FOPEN_READ_WRITE', 'r+b');
+defined('FOPEN_WRITE_CREATE_DESTRUCTIVE')       OR define('FOPEN_WRITE_CREATE_DESTRUCTIVE', 'wb'); // truncates existing file data, use with care
+defined('FOPEN_READ_WRITE_CREATE_DESTRUCTIVE')  OR define('FOPEN_READ_WRITE_CREATE_DESTRUCTIVE', 'w+b'); // truncates existing file data, use with care
+defined('FOPEN_WRITE_CREATE')                   OR define('FOPEN_WRITE_CREATE', 'ab');
+defined('FOPEN_READ_WRITE_CREATE')              OR define('FOPEN_READ_WRITE_CREATE', 'a+b');
+defined('FOPEN_WRITE_CREATE_STRICT')            OR define('FOPEN_WRITE_CREATE_STRICT', 'xb');
+defined('FOPEN_READ_WRITE_CREATE_STRICT')       OR define('FOPEN_READ_WRITE_CREATE_STRICT', 'x+b');
+
+/*
+|--------------------------------------------------------------------------
+| Exit Status Codes
+|--------------------------------------------------------------------------
+|
+| Used to indicate the conditions under which the script is exit()ing.
+| While there is no universal standard for error codes, there are some
+| broad conventions.  Three such conventions are mentioned below, for
+| those who wish to make use of them.  The CodeIgniter defaults were
+| chosen for the least overlap with these conventions, while still
+| leaving room for others to be defined in future versions and user
+| applications.
+|
+| The three main conventions used for determining exit status codes
+| are as follows:
+|
+|    Standard C/C++ Library (stdlibc):
+|       http://www.gnu.org/software/libc/manual/html_node/Exit-Status.html
+|       (This link also contains other GNU-specific conventions)
+|    BSD sysexits.h:
+|       http://www.gsp.com/cgi-bin/man.cgi?section=3&topic=sysexits
+|    Bash scripting:
+|       http://tldp.org/LDP/abs/html/exitcodes.html
+|
+*/
+defined('EXIT_SUCCESS')        OR define('EXIT_SUCCESS', 0); // no errors
+defined('EXIT_ERROR')          OR define('EXIT_ERROR', 1); // generic error
+defined('EXIT_CONFIG')         OR define('EXIT_CONFIG', 3); // configuration error
+defined('EXIT_UNKNOWN_FILE')   OR define('EXIT_UNKNOWN_FILE', 4); // file not found
+defined('EXIT_UNKNOWN_CLASS')  OR define('EXIT_UNKNOWN_CLASS', 5); // unknown class
+defined('EXIT_UNKNOWN_METHOD') OR define('EXIT_UNKNOWN_METHOD', 6); // unknown class member
+defined('EXIT_USER_INPUT')     OR define('EXIT_USER_INPUT', 7); // invalid user input
+defined('EXIT_DATABASE')       OR define('EXIT_DATABASE', 8); // database error
+defined('EXIT__AUTO_MIN')      OR define('EXIT__AUTO_MIN', 9); // lowest automatically-assigned error code
+defined('EXIT__AUTO_MAX')      OR define('EXIT__AUTO_MAX', 125); // highest automatically-assigned error code
diff --git a/application/config/database.php b/application/config/database.php
new file mode 100644
index 0000000..0088ef1
--- /dev/null
+++ b/application/config/database.php
@@ -0,0 +1,96 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/*
+| -------------------------------------------------------------------
+| DATABASE CONNECTIVITY SETTINGS
+| -------------------------------------------------------------------
+| This file will contain the settings needed to access your database.
+|
+| For complete instructions please consult the 'Database Connection'
+| page of the User Guide.
+|
+| -------------------------------------------------------------------
+| EXPLANATION OF VARIABLES
+| -------------------------------------------------------------------
+|
+|	['dsn']      The full DSN string describe a connection to the database.
+|	['hostname'] The hostname of your database server.
+|	['username'] The username used to connect to the database
+|	['password'] The password used to connect to the database
+|	['database'] The name of the database you want to connect to
+|	['dbdriver'] The database driver. e.g.: mysqli.
+|			Currently supported:
+|				 cubrid, ibase, mssql, mysql, mysqli, oci8,
+|				 odbc, pdo, postgre, sqlite, sqlite3, sqlsrv
+|	['dbprefix'] You can add an optional prefix, which will be added
+|				 to the table name when using the  Query Builder class
+|	['pconnect'] TRUE/FALSE - Whether to use a persistent connection
+|	['db_debug'] TRUE/FALSE - Whether database errors should be displayed.
+|	['cache_on'] TRUE/FALSE - Enables/disables query caching
+|	['cachedir'] The path to the folder where cache files should be stored
+|	['char_set'] The character set used in communicating with the database
+|	['dbcollat'] The character collation used in communicating with the database
+|				 NOTE: For MySQL and MySQLi databases, this setting is only used
+| 				 as a backup if your server is running PHP < 5.2.3 or MySQL < 5.0.7
+|				 (and in table creation queries made with DB Forge).
+| 				 There is an incompatibility in PHP with mysql_real_escape_string() which
+| 				 can make your site vulnerable to SQL injection if you are using a
+| 				 multi-byte character set and are running versions lower than these.
+| 				 Sites using Latin-1 or UTF-8 database character set and collation are unaffected.
+|	['swap_pre'] A default table prefix that should be swapped with the dbprefix
+|	['encrypt']  Whether or not to use an encrypted connection.
+|
+|			'mysql' (deprecated), 'sqlsrv' and 'pdo/sqlsrv' drivers accept TRUE/FALSE
+|			'mysqli' and 'pdo/mysql' drivers accept an array with the following options:
+|
+|				'ssl_key'    - Path to the private key file
+|				'ssl_cert'   - Path to the public key certificate file
+|				'ssl_ca'     - Path to the certificate authority file
+|				'ssl_capath' - Path to a directory containing trusted CA certificates in PEM format
+|				'ssl_cipher' - List of *allowed* ciphers to be used for the encryption, separated by colons (':')
+|				'ssl_verify' - TRUE/FALSE; Whether verify the server certificate or not
+|
+|	['compress'] Whether or not to use client compression (MySQL only)
+|	['stricton'] TRUE/FALSE - forces 'Strict Mode' connections
+|							- good for ensuring strict SQL while developing
+|	['ssl_options']	Used to set various SSL options that can be used when making SSL connections.
+|	['failover'] array - A array with 0 or more data for connections if the main should fail.
+|	['save_queries'] TRUE/FALSE - Whether to "save" all executed queries.
+| 				NOTE: Disabling this will also effectively disable both
+| 				$this->db->last_query() and profiling of DB queries.
+| 				When you run a query, with this setting set to TRUE (default),
+| 				CodeIgniter will store the SQL statement for debugging purposes.
+| 				However, this may cause high memory usage, especially if you run
+| 				a lot of SQL queries ... disable this to avoid that problem.
+|
+| The $active_group variable lets you choose which connection group to
+| make active.  By default there is only one group (the 'default' group).
+|
+| The $query_builder variables lets you determine whether or not to load
+| the query builder class.
+*/
+$active_group = 'default';
+$query_builder = TRUE;
+
+$db['default'] = array(
+	'dsn'	=> '',
+	'hostname' => 'localhost',
+	'username' => '',
+	'password' => '',
+	'database' => '',
+	'dbdriver' => 'mysqli',
+	'dbprefix' => '',
+	'pconnect' => FALSE,
+	'db_debug' => (ENVIRONMENT !== 'production'),
+	'cache_on' => FALSE,
+	'cachedir' => '',
+	'char_set' => 'utf8',
+	'dbcollat' => 'utf8_general_ci',
+	'swap_pre' => '',
+	'encrypt' => FALSE,
+	'compress' => FALSE,
+	'stricton' => FALSE,
+	'failover' => array(),
+	'save_queries' => TRUE
+);
diff --git a/application/config/doctypes.php b/application/config/doctypes.php
new file mode 100644
index 0000000..59a7991
--- /dev/null
+++ b/application/config/doctypes.php
@@ -0,0 +1,24 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+$_doctypes = array(
+	'xhtml11' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
+	'xhtml1-strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
+	'xhtml1-trans' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
+	'xhtml1-frame' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
+	'xhtml-basic11' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
+	'html5' => '<!DOCTYPE html>',
+	'html4-strict' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
+	'html4-trans' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
+	'html4-frame' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">',
+	'mathml1' => '<!DOCTYPE math SYSTEM "http://www.w3.org/Math/DTD/mathml1/mathml.dtd">',
+	'mathml2' => '<!DOCTYPE math PUBLIC "-//W3C//DTD MathML 2.0//EN" "http://www.w3.org/Math/DTD/mathml2/mathml2.dtd">',
+	'svg10' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">',
+	'svg11' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',
+	'svg11-basic' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Basic//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd">',
+	'svg11-tiny' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd">',
+	'xhtml-math-svg-xh' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">',
+	'xhtml-math-svg-sh' => '<!DOCTYPE svg:svg PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">',
+	'xhtml-rdfa-1' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">',
+	'xhtml-rdfa-2' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.1//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-2.dtd">'
+);
diff --git a/application/config/foreign_chars.php b/application/config/foreign_chars.php
new file mode 100644
index 0000000..0231f35
--- /dev/null
+++ b/application/config/foreign_chars.php
@@ -0,0 +1,114 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/*
+| -------------------------------------------------------------------
+| Foreign Characters
+| -------------------------------------------------------------------
+| This file contains an array of foreign characters for transliteration
+| conversion used by the Text helper
+|
+*/
+$foreign_characters = array(
+	'/ä|æ|ǽ/' => 'ae',
+	'/ö|œ/' => 'oe',
+	'/ü/' => 'ue',
+	'/Ä/' => 'Ae',
+	'/Ü/' => 'Ue',
+	'/Ö/' => 'Oe',
+	'/À|Á|Â|Ã|Ä|Å|Ǻ|Ā|Ă|Ą|Ǎ|Α|Ά|Ả|Ạ|Ầ|Ẫ|Ẩ|Ậ|Ằ|Ắ|Ẵ|Ẳ|Ặ|А/' => 'A',
+	'/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª|α|ά|ả|ạ|ầ|ấ|ẫ|ẩ|ậ|ằ|ắ|ẵ|ẳ|ặ|а/' => 'a',
+	'/Б/' => 'B',
+	'/б/' => 'b',
+	'/Ç|Ć|Ĉ|Ċ|Č/' => 'C',
+	'/ç|ć|ĉ|ċ|č/' => 'c',
+	'/Д|Δ/' => 'D',
+	'/д|δ/' => 'd',
+	'/Ð|Ď|Đ/' => 'Dj',
+	'/ð|ď|đ/' => 'dj',
+	'/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Ε|Έ|Ẽ|Ẻ|Ẹ|Ề|Ế|Ễ|Ể|Ệ|Е|Э/' => 'E',
+	'/è|é|ê|ë|ē|ĕ|ė|ę|ě|έ|ε|ẽ|ẻ|ẹ|ề|ế|ễ|ể|ệ|е|э/' => 'e',
+	'/Ф/' => 'F',
+	'/ф/' => 'f',
+	'/Ĝ|Ğ|Ġ|Ģ|Γ|Г|Ґ/' => 'G',
+	'/ĝ|ğ|ġ|ģ|γ|г|ґ/' => 'g',
+	'/Ĥ|Ħ/' => 'H',
+	'/ĥ|ħ/' => 'h',
+	'/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|Η|Ή|Ί|Ι|Ϊ|Ỉ|Ị|И|Ы/' => 'I',
+	'/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|η|ή|ί|ι|ϊ|ỉ|ị|и|ы|ї/' => 'i',
+	'/Ĵ/' => 'J',
+	'/ĵ/' => 'j',
+	'/Θ/' => 'TH',
+	'/θ/' => 'th',
+	'/Ķ|Κ|К/' => 'K',
+	'/ķ|κ|к/' => 'k',
+	'/Ĺ|Ļ|Ľ|Ŀ|Ł|Λ|Л/' => 'L',
+	'/ĺ|ļ|ľ|ŀ|ł|λ|л/' => 'l',
+	'/М/' => 'M',
+	'/м/' => 'm',
+	'/Ñ|Ń|Ņ|Ň|Ν|Н/' => 'N',
+	'/ñ|ń|ņ|ň|ʼn|ν|н/' => 'n',
+	'/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|Ο|Ό|Ω|Ώ|Ỏ|Ọ|Ồ|Ố|Ỗ|Ổ|Ộ|Ờ|Ớ|Ỡ|Ở|Ợ|О/' => 'O',
+	'/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|ο|ό|ω|ώ|ỏ|ọ|ồ|ố|ỗ|ổ|ộ|ờ|ớ|ỡ|ở|ợ|о/' => 'o',
+	'/П/' => 'P',
+	'/п/' => 'p',
+	'/Ŕ|Ŗ|Ř|Ρ|Р/' => 'R',
+	'/ŕ|ŗ|ř|ρ|р/' => 'r',
+	'/Ś|Ŝ|Ş|Ș|Š|Σ|С/' => 'S',
+	'/ś|ŝ|ş|ș|š|ſ|σ|ς|с/' => 's',
+	'/Ț|Ţ|Ť|Ŧ|Τ|Т/' => 'T',
+	'/ț|ţ|ť|ŧ|τ|т/' => 't',
+	'/Þ|þ/' => 'th',
+	'/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|Ũ|Ủ|Ụ|Ừ|Ứ|Ữ|Ử|Ự|У/' => 'U',
+	'/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ|υ|ύ|ϋ|ủ|ụ|ừ|ứ|ữ|ử|ự|у/' => 'u',
+	'/Ƴ|Ɏ|Ỵ|Ẏ|Ӳ|Ӯ|Ў|Ý|Ÿ|Ŷ|Υ|Ύ|Ϋ|Ỳ|Ỹ|Ỷ|Ỵ|Й/' => 'Y',
+	'/ẙ|ʏ|ƴ|ɏ|ỵ|ẏ|ӳ|ӯ|ў|ý|ÿ|ŷ|ỳ|ỹ|ỷ|ỵ|й/' => 'y',
+	'/В/' => 'V',
+	'/в/' => 'v',
+	'/Ŵ/' => 'W',
+	'/ŵ/' => 'w',
+	'/Φ/' => 'F',
+	'/φ/' => 'f',
+	'/Χ/' => 'CH',
+	'/χ/' => 'ch',
+	'/Ź|Ż|Ž|Ζ|З/' => 'Z',
+	'/ź|ż|ž|ζ|з/' => 'z',
+	'/Æ|Ǽ/' => 'AE',
+	'/ß/' => 'ss',
+	'/IJ/' => 'IJ',
+	'/ij/' => 'ij',
+	'/Œ/' => 'OE',
+	'/ƒ/' => 'f',
+	'/Ξ/' => 'KS',
+	'/ξ/' => 'ks',
+	'/Π/' => 'P',
+	'/π/' => 'p',
+	'/Β/' => 'V',
+	'/β/' => 'v',
+	'/Μ/' => 'M',
+	'/μ/' => 'm',
+	'/Ψ/' => 'PS',
+	'/ψ/' => 'ps',
+	'/Ё/' => 'Yo',
+	'/ё/' => 'yo',
+	'/Є/' => 'Ye',
+	'/є/' => 'ye',
+	'/Ї/' => 'Yi',
+	'/Ж/' => 'Zh',
+	'/ж/' => 'zh',
+	'/Х/' => 'Kh',
+	'/х/' => 'kh',
+	'/Ц/' => 'Ts',
+	'/ц/' => 'ts',
+	'/Ч/' => 'Ch',
+	'/ч/' => 'ch',
+	'/Ш/' => 'Sh',
+	'/ш/' => 'sh',
+	'/Щ/' => 'Shch',
+	'/щ/' => 'shch',
+	'/Ъ|ъ|Ь|ь/' => '',
+	'/Ю/' => 'Yu',
+	'/ю/' => 'yu',
+	'/Я/' => 'Ya',
+	'/я/' => 'ya'
+);
diff --git a/application/config/hooks.php b/application/config/hooks.php
new file mode 100644
index 0000000..79c5c16
--- /dev/null
+++ b/application/config/hooks.php
@@ -0,0 +1,13 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/*
+| -------------------------------------------------------------------------
+| Hooks
+| -------------------------------------------------------------------------
+| This file lets you define "hooks" to extend CI without hacking the core
+| files.  Please see the user guide for info:
+|
+|	https://codeigniter.com/userguide3/general/hooks.html
+|
+*/
diff --git a/application/config/index.html b/application/config/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/config/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/application/config/memcached.php b/application/config/memcached.php
new file mode 100644
index 0000000..65a1496
--- /dev/null
+++ b/application/config/memcached.php
@@ -0,0 +1,19 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/*
+| -------------------------------------------------------------------------
+| Memcached settings
+| -------------------------------------------------------------------------
+| Your Memcached servers can be specified below.
+|
+|	See: https://codeigniter.com/userguide3/libraries/caching.html#memcached
+|
+*/
+$config = array(
+	'default' => array(
+		'hostname' => '127.0.0.1',
+		'port'     => '11211',
+		'weight'   => '1',
+	),
+);
diff --git a/application/config/migration.php b/application/config/migration.php
new file mode 100644
index 0000000..4b585a6
--- /dev/null
+++ b/application/config/migration.php
@@ -0,0 +1,84 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/*
+|--------------------------------------------------------------------------
+| Enable/Disable Migrations
+|--------------------------------------------------------------------------
+|
+| Migrations are disabled by default for security reasons.
+| You should enable migrations whenever you intend to do a schema migration
+| and disable it back when you're done.
+|
+*/
+$config['migration_enabled'] = FALSE;
+
+/*
+|--------------------------------------------------------------------------
+| Migration Type
+|--------------------------------------------------------------------------
+|
+| Migration file names may be based on a sequential identifier or on
+| a timestamp. Options are:
+|
+|   'sequential' = Sequential migration naming (001_add_blog.php)
+|   'timestamp'  = Timestamp migration naming (20121031104401_add_blog.php)
+|                  Use timestamp format YYYYMMDDHHIISS.
+|
+| Note: If this configuration value is missing the Migration library
+|       defaults to 'sequential' for backward compatibility with CI2.
+|
+*/
+$config['migration_type'] = 'timestamp';
+
+/*
+|--------------------------------------------------------------------------
+| Migrations table
+|--------------------------------------------------------------------------
+|
+| This is the name of the table that will store the current migrations state.
+| When migrations runs it will store in a database table which migration
+| level the system is at. It then compares the migration level in this
+| table to the $config['migration_version'] if they are not the same it
+| will migrate up. This must be set.
+|
+*/
+$config['migration_table'] = 'migrations';
+
+/*
+|--------------------------------------------------------------------------
+| Auto Migrate To Latest
+|--------------------------------------------------------------------------
+|
+| If this is set to TRUE when you load the migrations class and have
+| $config['migration_enabled'] set to TRUE the system will auto migrate
+| to your latest migration (whatever $config['migration_version'] is
+| set to). This way you do not have to call migrations anywhere else
+| in your code to have the latest migration.
+|
+*/
+$config['migration_auto_latest'] = FALSE;
+
+/*
+|--------------------------------------------------------------------------
+| Migrations version
+|--------------------------------------------------------------------------
+|
+| This is used to set migration version that the file system should be on.
+| If you run $this->migration->current() this is the version that schema will
+| be upgraded / downgraded to.
+|
+*/
+$config['migration_version'] = 0;
+
+/*
+|--------------------------------------------------------------------------
+| Migrations Path
+|--------------------------------------------------------------------------
+|
+| Path to your migrations folder.
+| Typically, it will be within your application path.
+| Also, writing permission is required within the migrations path.
+|
+*/
+$config['migration_path'] = APPPATH.'migrations/';
diff --git a/application/config/mimes.php b/application/config/mimes.php
new file mode 100644
index 0000000..b2e989f
--- /dev/null
+++ b/application/config/mimes.php
@@ -0,0 +1,186 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/*
+| -------------------------------------------------------------------
+| MIME TYPES
+| -------------------------------------------------------------------
+| This file contains an array of mime types. It is used by the
+| Upload class to help identify allowed file types.
+|
+*/
+return array(
+	'hqx'	=>	array('application/mac-binhex40', 'application/mac-binhex', 'application/x-binhex40', 'application/x-mac-binhex40'),
+	'cpt'	=>	'application/mac-compactpro',
+	'csv'	=>	array('text/x-comma-separated-values', 'text/comma-separated-values', 'application/octet-stream', 'application/vnd.ms-excel', 'application/x-csv', 'text/x-csv', 'text/csv', 'application/csv', 'application/excel', 'application/vnd.msexcel', 'text/plain'),
+	'bin'	=>	array('application/macbinary', 'application/mac-binary', 'application/octet-stream', 'application/x-binary', 'application/x-macbinary'),
+	'dms'	=>	'application/octet-stream',
+	'lha'	=>	'application/octet-stream',
+	'lzh'	=>	'application/octet-stream',
+	'exe'	=>	array('application/octet-stream', 'application/x-msdownload'),
+	'class'	=>	'application/octet-stream',
+	'psd'	=>	array('application/x-photoshop', 'image/vnd.adobe.photoshop'),
+	'so'	=>	'application/octet-stream',
+	'sea'	=>	'application/octet-stream',
+	'dll'	=>	'application/octet-stream',
+	'oda'	=>	'application/oda',
+	'pdf'	=>	array('application/pdf', 'application/force-download', 'application/x-download', 'binary/octet-stream'),
+	'ai'	=>	array('application/pdf', 'application/postscript'),
+	'eps'	=>	'application/postscript',
+	'ps'	=>	'application/postscript',
+	'smi'	=>	'application/smil',
+	'smil'	=>	'application/smil',
+	'mif'	=>	'application/vnd.mif',
+	'xls'	=>	array('application/vnd.ms-excel', 'application/msexcel', 'application/x-msexcel', 'application/x-ms-excel', 'application/x-excel', 'application/x-dos_ms_excel', 'application/xls', 'application/x-xls', 'application/excel', 'application/download', 'application/vnd.ms-office', 'application/msword'),
+	'ppt'	=>	array('application/powerpoint', 'application/vnd.ms-powerpoint', 'application/vnd.ms-office', 'application/msword'),
+	'pptx'	=> 	array('application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/x-zip', 'application/zip'),
+	'wbxml'	=>	'application/wbxml',
+	'wmlc'	=>	'application/wmlc',
+	'dcr'	=>	'application/x-director',
+	'dir'	=>	'application/x-director',
+	'dxr'	=>	'application/x-director',
+	'dvi'	=>	'application/x-dvi',
+	'gtar'	=>	'application/x-gtar',
+	'gz'	=>	'application/x-gzip',
+	'gzip'  =>	'application/x-gzip',
+	'php'	=>	array('application/x-httpd-php', 'application/php', 'application/x-php', 'text/php', 'text/x-php', 'application/x-httpd-php-source'),
+	'php4'	=>	'application/x-httpd-php',
+	'php3'	=>	'application/x-httpd-php',
+	'phtml'	=>	'application/x-httpd-php',
+	'phps'	=>	'application/x-httpd-php-source',
+	'js'	=>	array('application/x-javascript', 'text/plain'),
+	'swf'	=>	'application/x-shockwave-flash',
+	'sit'	=>	'application/x-stuffit',
+	'tar'	=>	'application/x-tar',
+	'tgz'	=>	array('application/x-tar', 'application/x-gzip-compressed'),
+	'z'	=>	'application/x-compress',
+	'xhtml'	=>	'application/xhtml+xml',
+	'xht'	=>	'application/xhtml+xml',
+	'zip'	=>	array('application/x-zip', 'application/zip', 'application/x-zip-compressed', 'application/s-compressed', 'multipart/x-zip'),
+	'rar'	=>	array('application/x-rar', 'application/rar', 'application/x-rar-compressed'),
+	'mid'	=>	'audio/midi',
+	'midi'	=>	'audio/midi',
+	'mpga'	=>	'audio/mpeg',
+	'mp2'	=>	'audio/mpeg',
+	'mp3'	=>	array('audio/mpeg', 'audio/mpg', 'audio/mpeg3', 'audio/mp3'),
+	'aif'	=>	array('audio/x-aiff', 'audio/aiff'),
+	'aiff'	=>	array('audio/x-aiff', 'audio/aiff'),
+	'aifc'	=>	'audio/x-aiff',
+	'ram'	=>	'audio/x-pn-realaudio',
+	'rm'	=>	'audio/x-pn-realaudio',
+	'rpm'	=>	'audio/x-pn-realaudio-plugin',
+	'ra'	=>	'audio/x-realaudio',
+	'rv'	=>	'video/vnd.rn-realvideo',
+	'wav'	=>	array('audio/x-wav', 'audio/wave', 'audio/wav'),
+	'bmp'	=>	array('image/bmp', 'image/x-bmp', 'image/x-bitmap', 'image/x-xbitmap', 'image/x-win-bitmap', 'image/x-windows-bmp', 'image/ms-bmp', 'image/x-ms-bmp', 'application/bmp', 'application/x-bmp', 'application/x-win-bitmap'),
+	'gif'	=>	'image/gif',
+	'jpeg'	=>	array('image/jpeg', 'image/pjpeg'),
+	'jpg'	=>	array('image/jpeg', 'image/pjpeg'),
+	'jpe'	=>	array('image/jpeg', 'image/pjpeg'),
+	'jp2'	=>	array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'),
+	'j2k'	=>	array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'),
+	'jpf'	=>	array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'),
+	'jpg2'	=>	array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'),
+	'jpx'	=>	array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'),
+	'jpm'	=>	array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'),
+	'mj2'	=>	array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'),
+	'mjp2'	=>	array('image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'),
+	'png'	=>	array('image/png', 'image/x-png'),
+	'tiff'	=>	'image/tiff',
+	'tif'	=>	'image/tiff',
+	'heic' 	=>	'image/heic',
+	'heif' 	=>	'image/heif',
+	'css'	=>	array('text/css', 'text/plain'),
+	'html'	=>	array('text/html', 'text/plain'),
+	'htm'	=>	array('text/html', 'text/plain'),
+	'shtml'	=>	array('text/html', 'text/plain'),
+	'txt'	=>	'text/plain',
+	'text'	=>	'text/plain',
+	'log'	=>	array('text/plain', 'text/x-log'),
+	'rtx'	=>	'text/richtext',
+	'rtf'	=>	'text/rtf',
+	'xml'	=>	array('application/xml', 'text/xml', 'text/plain'),
+	'xsl'	=>	array('application/xml', 'text/xsl', 'text/xml'),
+	'mpeg'	=>	'video/mpeg',
+	'mpg'	=>	'video/mpeg',
+	'mpe'	=>	'video/mpeg',
+	'qt'	=>	'video/quicktime',
+	'mov'	=>	'video/quicktime',
+	'avi'	=>	array('video/x-msvideo', 'video/msvideo', 'video/avi', 'application/x-troff-msvideo'),
+	'movie'	=>	'video/x-sgi-movie',
+	'doc'	=>	array('application/msword', 'application/vnd.ms-office'),
+	'docx'	=>	array('application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/zip', 'application/msword', 'application/x-zip'),
+	'dot'	=>	array('application/msword', 'application/vnd.ms-office'),
+	'dotx'	=>	array('application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/zip', 'application/msword'),
+	'xlsx'	=>	array('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/zip', 'application/vnd.ms-excel', 'application/msword', 'application/x-zip'),
+	'word'	=>	array('application/msword', 'application/octet-stream'),
+	'xl'	=>	'application/excel',
+	'eml'	=>	'message/rfc822',
+	'json'  =>	array('application/json', 'text/json'),
+	'pem'   =>	array('application/x-x509-user-cert', 'application/x-pem-file', 'application/octet-stream'),
+	'p10'   =>	array('application/x-pkcs10', 'application/pkcs10'),
+	'p12'   =>	'application/x-pkcs12',
+	'p7a'   =>	'application/x-pkcs7-signature',
+	'p7c'   =>	array('application/pkcs7-mime', 'application/x-pkcs7-mime'),
+	'p7m'   =>	array('application/pkcs7-mime', 'application/x-pkcs7-mime'),
+	'p7r'   =>	'application/x-pkcs7-certreqresp',
+	'p7s'   =>	'application/pkcs7-signature',
+	'crt'   =>	array('application/x-x509-ca-cert', 'application/x-x509-user-cert', 'application/pkix-cert'),
+	'crl'   =>	array('application/pkix-crl', 'application/pkcs-crl'),
+	'der'   =>	'application/x-x509-ca-cert',
+	'kdb'   =>	'application/octet-stream',
+	'pgp'   =>	'application/pgp',
+	'gpg'   =>	'application/gpg-keys',
+	'sst'   =>	'application/octet-stream',
+	'csr'   =>	'application/octet-stream',
+	'rsa'   =>	'application/x-pkcs7',
+	'cer'   =>	array('application/pkix-cert', 'application/x-x509-ca-cert'),
+	'3g2'   =>	'video/3gpp2',
+	'3gp'   =>	array('video/3gp', 'video/3gpp'),
+	'mp4'   =>	'video/mp4',
+	'm4a'   =>	'audio/x-m4a',
+	'f4v'   =>	array('video/mp4', 'video/x-f4v'),
+	'flv'	=>	'video/x-flv',
+	'webm'	=>	'video/webm',
+	'aac'   =>	array('audio/x-aac', 'audio/aac'),
+	'm4u'   =>	'application/vnd.mpegurl',
+	'm3u'   =>	'text/plain',
+	'xspf'  =>	'application/xspf+xml',
+	'vlc'   =>	'application/videolan',
+	'wmv'   =>	array('video/x-ms-wmv', 'video/x-ms-asf'),
+	'au'    =>	'audio/x-au',
+	'ac3'   =>	'audio/ac3',
+	'flac'  =>	'audio/x-flac',
+	'ogg'   =>	array('audio/ogg', 'video/ogg', 'application/ogg'),
+	'kmz'	=>	array('application/vnd.google-earth.kmz', 'application/zip', 'application/x-zip'),
+	'kml'	=>	array('application/vnd.google-earth.kml+xml', 'application/xml', 'text/xml'),
+	'ics'	=>	'text/calendar',
+	'ical'	=>	'text/calendar',
+	'zsh'	=>	'text/x-scriptzsh',
+	'7z'	=>	array('application/x-7z-compressed', 'application/x-compressed', 'application/x-zip-compressed', 'application/zip', 'multipart/x-zip'),
+	'7zip'	=>	array('application/x-7z-compressed', 'application/x-compressed', 'application/x-zip-compressed', 'application/zip', 'multipart/x-zip'),
+	'cdr'	=>	array('application/cdr', 'application/coreldraw', 'application/x-cdr', 'application/x-coreldraw', 'image/cdr', 'image/x-cdr', 'zz-application/zz-winassoc-cdr'),
+	'wma'	=>	array('audio/x-ms-wma', 'video/x-ms-asf'),
+	'jar'	=>	array('application/java-archive', 'application/x-java-application', 'application/x-jar', 'application/x-compressed'),
+	'svg'	=>	array('image/svg+xml', 'image/svg', 'application/xml', 'text/xml'),
+	'vcf'	=>	'text/x-vcard',
+	'srt'	=>	array('text/srt', 'text/plain'),
+	'vtt'	=>	array('text/vtt', 'text/plain'),
+	'ico'	=>	array('image/x-icon', 'image/x-ico', 'image/vnd.microsoft.icon'),
+	'odc'	=>	'application/vnd.oasis.opendocument.chart',
+	'otc'	=>	'application/vnd.oasis.opendocument.chart-template',
+	'odf'	=>	'application/vnd.oasis.opendocument.formula',
+	'otf'	=>	'application/vnd.oasis.opendocument.formula-template',
+	'odg'	=>	'application/vnd.oasis.opendocument.graphics',
+	'otg'	=>	'application/vnd.oasis.opendocument.graphics-template',
+	'odi'	=>	'application/vnd.oasis.opendocument.image',
+	'oti'	=>	'application/vnd.oasis.opendocument.image-template',
+	'odp'	=>	'application/vnd.oasis.opendocument.presentation',
+	'otp'	=>	'application/vnd.oasis.opendocument.presentation-template',
+	'ods'	=>	'application/vnd.oasis.opendocument.spreadsheet',
+	'ots'	=>	'application/vnd.oasis.opendocument.spreadsheet-template',
+	'odt'	=>	'application/vnd.oasis.opendocument.text',
+	'odm'	=>	'application/vnd.oasis.opendocument.text-master',
+	'ott'	=>	'application/vnd.oasis.opendocument.text-template',
+	'oth'	=>	'application/vnd.oasis.opendocument.text-web'
+);
diff --git a/application/config/profiler.php b/application/config/profiler.php
new file mode 100644
index 0000000..3436e93
--- /dev/null
+++ b/application/config/profiler.php
@@ -0,0 +1,14 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/*
+| -------------------------------------------------------------------------
+| Profiler Sections
+| -------------------------------------------------------------------------
+| This file lets you determine whether or not various sections of Profiler
+| data are displayed when the Profiler is enabled.
+| Please see the user guide for info:
+|
+|	https://codeigniter.com/userguide3/general/profiling.html
+|
+*/
diff --git a/application/config/routes.php b/application/config/routes.php
new file mode 100644
index 0000000..e8e2296
--- /dev/null
+++ b/application/config/routes.php
@@ -0,0 +1,54 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/*
+| -------------------------------------------------------------------------
+| URI ROUTING
+| -------------------------------------------------------------------------
+| This file lets you re-map URI requests to specific controller functions.
+|
+| Typically there is a one-to-one relationship between a URL string
+| and its corresponding controller class/method. The segments in a
+| URL normally follow this pattern:
+|
+|	example.com/class/method/id/
+|
+| In some instances, however, you may want to remap this relationship
+| so that a different class/function is called than the one
+| corresponding to the URL.
+|
+| Please see the user guide for complete details:
+|
+|	https://codeigniter.com/userguide3/general/routing.html
+|
+| -------------------------------------------------------------------------
+| RESERVED ROUTES
+| -------------------------------------------------------------------------
+|
+| There are three reserved routes:
+|
+|	$route['default_controller'] = 'welcome';
+|
+| This route indicates which controller class should be loaded if the
+| URI contains no data. In the above example, the "welcome" class
+| would be loaded.
+|
+|	$route['404_override'] = 'errors/page_missing';
+|
+| This route will tell the Router which controller/method to use if those
+| provided in the URL cannot be matched to a valid route.
+|
+|	$route['translate_uri_dashes'] = FALSE;
+|
+| This is not exactly a route, but allows you to automatically route
+| controller and method names that contain dashes. '-' isn't a valid
+| class or method name character, so it requires translation.
+| When you set this option to TRUE, it will replace ALL dashes in the
+| controller and method URI segments.
+|
+| Examples:	my-controller/index	-> my_controller/index
+|		my-controller/my-method	-> my_controller/my_method
+*/
+$route['default_controller'] = 'welcome';
+$route['404_override'] = '';
+$route['translate_uri_dashes'] = FALSE;
diff --git a/application/config/smileys.php b/application/config/smileys.php
new file mode 100644
index 0000000..a9b9191
--- /dev/null
+++ b/application/config/smileys.php
@@ -0,0 +1,64 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/*
+| -------------------------------------------------------------------
+| SMILEYS
+| -------------------------------------------------------------------
+| This file contains an array of smileys for use with the emoticon helper.
+| Individual images can be used to replace multiple smileys.  For example:
+| :-) and :) use the same image replacement.
+|
+| Please see user guide for more info:
+| https://codeigniter.com/userguide3/helpers/smiley_helper.html
+|
+*/
+$smileys = array(
+
+//	smiley			image name						width	height	alt
+
+	':-)'			=>	array('grin.gif',			'19',	'19',	'grin'),
+	':lol:'			=>	array('lol.gif',			'19',	'19',	'LOL'),
+	':cheese:'		=>	array('cheese.gif',			'19',	'19',	'cheese'),
+	':)'			=>	array('smile.gif',			'19',	'19',	'smile'),
+	';-)'			=>	array('wink.gif',			'19',	'19',	'wink'),
+	';)'			=>	array('wink.gif',			'19',	'19',	'wink'),
+	':smirk:'		=>	array('smirk.gif',			'19',	'19',	'smirk'),
+	':roll:'		=>	array('rolleyes.gif',		'19',	'19',	'rolleyes'),
+	':-S'			=>	array('confused.gif',		'19',	'19',	'confused'),
+	':wow:'			=>	array('surprise.gif',		'19',	'19',	'surprised'),
+	':bug:'			=>	array('bigsurprise.gif',	'19',	'19',	'big surprise'),
+	':-P'			=>	array('tongue_laugh.gif',	'19',	'19',	'tongue laugh'),
+	'%-P'			=>	array('tongue_rolleye.gif',	'19',	'19',	'tongue rolleye'),
+	';-P'			=>	array('tongue_wink.gif',	'19',	'19',	'tongue wink'),
+	':P'			=>	array('raspberry.gif',		'19',	'19',	'raspberry'),
+	':blank:'		=>	array('blank.gif',			'19',	'19',	'blank stare'),
+	':long:'		=>	array('longface.gif',		'19',	'19',	'long face'),
+	':ohh:'			=>	array('ohh.gif',			'19',	'19',	'ohh'),
+	':grrr:'		=>	array('grrr.gif',			'19',	'19',	'grrr'),
+	':gulp:'		=>	array('gulp.gif',			'19',	'19',	'gulp'),
+	'8-/'			=>	array('ohoh.gif',			'19',	'19',	'oh oh'),
+	':down:'		=>	array('downer.gif',			'19',	'19',	'downer'),
+	':red:'			=>	array('embarrassed.gif',	'19',	'19',	'red face'),
+	':sick:'		=>	array('sick.gif',			'19',	'19',	'sick'),
+	':shut:'		=>	array('shuteye.gif',		'19',	'19',	'shut eye'),
+	':-/'			=>	array('hmm.gif',			'19',	'19',	'hmmm'),
+	'>:('			=>	array('mad.gif',			'19',	'19',	'mad'),
+	':mad:'			=>	array('mad.gif',			'19',	'19',	'mad'),
+	'>:-('			=>	array('angry.gif',			'19',	'19',	'angry'),
+	':angry:'		=>	array('angry.gif',			'19',	'19',	'angry'),
+	':zip:'			=>	array('zip.gif',			'19',	'19',	'zipper'),
+	':kiss:'		=>	array('kiss.gif',			'19',	'19',	'kiss'),
+	':ahhh:'		=>	array('shock.gif',			'19',	'19',	'shock'),
+	':coolsmile:'	=>	array('shade_smile.gif',	'19',	'19',	'cool smile'),
+	':coolsmirk:'	=>	array('shade_smirk.gif',	'19',	'19',	'cool smirk'),
+	':coolgrin:'	=>	array('shade_grin.gif',		'19',	'19',	'cool grin'),
+	':coolhmm:'		=>	array('shade_hmm.gif',		'19',	'19',	'cool hmm'),
+	':coolmad:'		=>	array('shade_mad.gif',		'19',	'19',	'cool mad'),
+	':coolcheese:'	=>	array('shade_cheese.gif',	'19',	'19',	'cool cheese'),
+	':vampire:'		=>	array('vampire.gif',		'19',	'19',	'vampire'),
+	':snake:'		=>	array('snake.gif',			'19',	'19',	'snake'),
+	':exclaim:'		=>	array('exclaim.gif',		'19',	'19',	'exclaim'),
+	':question:'	=>	array('question.gif',		'19',	'19',	'question')
+
+);
diff --git a/application/config/user_agents.php b/application/config/user_agents.php
new file mode 100644
index 0000000..5e1f6af
--- /dev/null
+++ b/application/config/user_agents.php
@@ -0,0 +1,222 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/*
+| -------------------------------------------------------------------
+| USER AGENT TYPES
+| -------------------------------------------------------------------
+| This file contains four arrays of user agent data. It is used by the
+| User Agent Class to help identify browser, platform, robot, and
+| mobile device data. The array keys are used to identify the device
+| and the array values are used to set the actual name of the item.
+*/
+$platforms = array(
+	'windows nt 10.0'	=> 'Windows 10',
+	'windows nt 6.3'	=> 'Windows 8.1',
+	'windows nt 6.2'	=> 'Windows 8',
+	'windows nt 6.1'	=> 'Windows 7',
+	'windows nt 6.0'	=> 'Windows Vista',
+	'windows nt 5.2'	=> 'Windows 2003',
+	'windows nt 5.1'	=> 'Windows XP',
+	'windows nt 5.0'	=> 'Windows 2000',
+	'windows nt 4.0'	=> 'Windows NT 4.0',
+	'winnt4.0'			=> 'Windows NT 4.0',
+	'winnt 4.0'			=> 'Windows NT',
+	'winnt'				=> 'Windows NT',
+	'windows 98'		=> 'Windows 98',
+	'win98'				=> 'Windows 98',
+	'windows 95'		=> 'Windows 95',
+	'win95'				=> 'Windows 95',
+	'windows phone'			=> 'Windows Phone',
+	'windows'			=> 'Unknown Windows OS',
+	'android'			=> 'Android',
+	'blackberry'		=> 'BlackBerry',
+	'iphone'			=> 'iOS',
+	'ipad'				=> 'iOS',
+	'ipod'				=> 'iOS',
+	'os x'				=> 'Mac OS X',
+	'ppc mac'			=> 'Power PC Mac',
+	'freebsd'			=> 'FreeBSD',
+	'ppc'				=> 'Macintosh',
+	'linux'				=> 'Linux',
+	'debian'			=> 'Debian',
+	'sunos'				=> 'Sun Solaris',
+	'beos'				=> 'BeOS',
+	'apachebench'		=> 'ApacheBench',
+	'aix'				=> 'AIX',
+	'irix'				=> 'Irix',
+	'osf'				=> 'DEC OSF',
+	'hp-ux'				=> 'HP-UX',
+	'netbsd'			=> 'NetBSD',
+	'bsdi'				=> 'BSDi',
+	'openbsd'			=> 'OpenBSD',
+	'gnu'				=> 'GNU/Linux',
+	'unix'				=> 'Unknown Unix OS',
+	'symbian' 			=> 'Symbian OS'
+);
+
+
+// The order of this array should NOT be changed. Many browsers return
+// multiple browser types so we want to identify the sub-type first.
+$browsers = array(
+	'OPR'			=> 'Opera',
+	'Flock'			=> 'Flock',
+	'Edge'			=> 'Edge',
+	'Chrome'		=> 'Chrome',
+	// Opera 10+ always reports Opera/9.80 and appends Version/<real version> to the user agent string
+	'Opera.*?Version'	=> 'Opera',
+	'Opera'			=> 'Opera',
+	'MSIE'			=> 'Internet Explorer',
+	'Internet Explorer'	=> 'Internet Explorer',
+	'Trident.* rv'	=> 'Internet Explorer',
+	'Shiira'		=> 'Shiira',
+	'Firefox'		=> 'Firefox',
+	'Chimera'		=> 'Chimera',
+	'Phoenix'		=> 'Phoenix',
+	'Firebird'		=> 'Firebird',
+	'Camino'		=> 'Camino',
+	'Netscape'		=> 'Netscape',
+	'OmniWeb'		=> 'OmniWeb',
+	'Safari'		=> 'Safari',
+	'Mozilla'		=> 'Mozilla',
+	'Konqueror'		=> 'Konqueror',
+	'icab'			=> 'iCab',
+	'Lynx'			=> 'Lynx',
+	'Links'			=> 'Links',
+	'hotjava'		=> 'HotJava',
+	'amaya'			=> 'Amaya',
+	'IBrowse'		=> 'IBrowse',
+	'Maxthon'		=> 'Maxthon',
+	'Ubuntu'		=> 'Ubuntu Web Browser'
+);
+
+$mobiles = array(
+	// legacy array, old values commented out
+	'mobileexplorer'	=> 'Mobile Explorer',
+//  'openwave'			=> 'Open Wave',
+//	'opera mini'		=> 'Opera Mini',
+//	'operamini'			=> 'Opera Mini',
+//	'elaine'			=> 'Palm',
+	'palmsource'		=> 'Palm',
+//	'digital paths'		=> 'Palm',
+//	'avantgo'			=> 'Avantgo',
+//	'xiino'				=> 'Xiino',
+	'palmscape'			=> 'Palmscape',
+//	'nokia'				=> 'Nokia',
+//	'ericsson'			=> 'Ericsson',
+//	'blackberry'		=> 'BlackBerry',
+//	'motorola'			=> 'Motorola'
+
+	// Phones and Manufacturers
+	'motorola'		=> 'Motorola',
+	'nokia'			=> 'Nokia',
+	'nexus'			=> 'Nexus',
+	'palm'			=> 'Palm',
+	'iphone'		=> 'Apple iPhone',
+	'ipad'			=> 'iPad',
+	'ipod'			=> 'Apple iPod Touch',
+	'sony'			=> 'Sony Ericsson',
+	'ericsson'		=> 'Sony Ericsson',
+	'blackberry'	=> 'BlackBerry',
+	'cocoon'		=> 'O2 Cocoon',
+	'blazer'		=> 'Treo',
+	'lg'			=> 'LG',
+	'amoi'			=> 'Amoi',
+	'xda'			=> 'XDA',
+	'mda'			=> 'MDA',
+	'vario'			=> 'Vario',
+	'htc'			=> 'HTC',
+	'samsung'		=> 'Samsung',
+	'sharp'			=> 'Sharp',
+	'sie-'			=> 'Siemens',
+	'alcatel'		=> 'Alcatel',
+	'benq'			=> 'BenQ',
+	'ipaq'			=> 'HP iPaq',
+	'mot-'			=> 'Motorola',
+	'playstation portable'	=> 'PlayStation Portable',
+	'playstation 3'		=> 'PlayStation 3',
+	'playstation vita'  	=> 'PlayStation Vita',
+	'hiptop'		=> 'Danger Hiptop',
+	'nec-'			=> 'NEC',
+	'panasonic'		=> 'Panasonic',
+	'philips'		=> 'Philips',
+	'sagem'			=> 'Sagem',
+	'sanyo'			=> 'Sanyo',
+	'spv'			=> 'SPV',
+	'zte'			=> 'ZTE',
+	'sendo'			=> 'Sendo',
+	'nintendo dsi'	=> 'Nintendo DSi',
+	'nintendo ds'	=> 'Nintendo DS',
+	'nintendo 3ds'	=> 'Nintendo 3DS',
+	'wii'			=> 'Nintendo Wii',
+	'open web'		=> 'Open Web',
+	'openweb'		=> 'OpenWeb',
+	'meizu'                 => 'Meizu',
+	'huawei'                => 'Huawei',
+	'xiaomi'                => 'Xiaomi',
+	'oppo'                  => 'Oppo',
+	'vivo'                  => 'Vivo',
+	'infinix'               => 'Infinix',
+
+	// Operating Systems
+	'android'		=> 'Android',
+	'symbian'		=> 'Symbian',
+	'SymbianOS'		=> 'SymbianOS',
+	'elaine'		=> 'Palm',
+	'series60'		=> 'Symbian S60',
+	'windows ce'	=> 'Windows CE',
+
+	// Browsers
+	'obigo'			=> 'Obigo',
+	'netfront'		=> 'Netfront Browser',
+	'openwave'		=> 'Openwave Browser',
+	'mobilexplorer'	=> 'Mobile Explorer',
+	'operamini'		=> 'Opera Mini',
+	'opera mini'	=> 'Opera Mini',
+	'opera mobi'	=> 'Opera Mobile',
+	'fennec'		=> 'Firefox Mobile',
+
+	// Other
+	'digital paths'	=> 'Digital Paths',
+	'avantgo'		=> 'AvantGo',
+	'xiino'			=> 'Xiino',
+	'novarra'		=> 'Novarra Transcoder',
+	'vodafone'		=> 'Vodafone',
+	'docomo'		=> 'NTT DoCoMo',
+	'o2'			=> 'O2',
+
+	// Fallback
+	'mobile'		=> 'Generic Mobile',
+	'wireless'		=> 'Generic Mobile',
+	'j2me'			=> 'Generic Mobile',
+	'midp'			=> 'Generic Mobile',
+	'cldc'			=> 'Generic Mobile',
+	'up.link'		=> 'Generic Mobile',
+	'up.browser'	=> 'Generic Mobile',
+	'smartphone'	=> 'Generic Mobile',
+	'cellphone'		=> 'Generic Mobile'
+);
+
+// There are hundreds of bots but these are the most common.
+$robots = array(
+	'googlebot'		=> 'Googlebot',
+	'msnbot'		=> 'MSNBot',
+	'baiduspider'		=> 'Baiduspider',
+	'bingbot'		=> 'Bing',
+	'slurp'			=> 'Inktomi Slurp',
+	'yahoo'			=> 'Yahoo',
+	'ask jeeves'		=> 'Ask Jeeves',
+	'fastcrawler'		=> 'FastCrawler',
+	'infoseek'		=> 'InfoSeek Robot 1.0',
+	'lycos'			=> 'Lycos',
+	'yandex'		=> 'YandexBot',
+	'mediapartners-google'	=> 'MediaPartners Google',
+	'CRAZYWEBCRAWLER'	=> 'Crazy Webcrawler',
+	'adsbot-google'		=> 'AdsBot Google',
+	'feedfetcher-google'	=> 'Feedfetcher Google',
+	'curious george'	=> 'Curious George',
+	'ia_archiver'		=> 'Alexa Crawler',
+	'MJ12bot'		=> 'Majestic-12',
+	'Uptimebot'		=> 'Uptimebot',
+	'UptimeRobot'		=> 'UptimeRobot'
+);
diff --git a/application/controllers/Welcome.php b/application/controllers/Welcome.php
new file mode 100644
index 0000000..5f82771
--- /dev/null
+++ b/application/controllers/Welcome.php
@@ -0,0 +1,25 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+class Welcome extends CI_Controller {
+
+	/**
+	 * Index Page for this controller.
+	 *
+	 * Maps to the following URL
+	 * 		http://example.com/index.php/welcome
+	 *	- or -
+	 * 		http://example.com/index.php/welcome/index
+	 *	- or -
+	 * Since this controller is set as the default controller in
+	 * config/routes.php, it's displayed at http://example.com/
+	 *
+	 * So any other public methods not prefixed with an underscore will
+	 * map to /index.php/welcome/<method_name>
+	 * @see https://codeigniter.com/userguide3/general/urls.html
+	 */
+	public function index()
+	{
+		$this->load->view('welcome_message');
+	}
+}
diff --git a/application/controllers/index.html b/application/controllers/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/controllers/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/application/core/index.html b/application/core/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/core/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/application/helpers/index.html b/application/helpers/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/helpers/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/application/hooks/index.html b/application/hooks/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/hooks/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/application/index.html b/application/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/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/application/language/english/index.html b/application/language/english/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/language/english/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/application/language/index.html b/application/language/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/language/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/application/libraries/index.html b/application/libraries/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/libraries/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/application/logs/index.html b/application/logs/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/logs/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/application/models/index.html b/application/models/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/models/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/application/third_party/index.html b/application/third_party/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/third_party/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/application/views/errors/cli/error_404.php b/application/views/errors/cli/error_404.php
new file mode 100644
index 0000000..6984b61
--- /dev/null
+++ b/application/views/errors/cli/error_404.php
@@ -0,0 +1,8 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+echo "\nERROR: ",
+	$heading,
+	"\n\n",
+	$message,
+	"\n\n";
\ No newline at end of file
diff --git a/application/views/errors/cli/error_db.php b/application/views/errors/cli/error_db.php
new file mode 100644
index 0000000..2ff43ff
--- /dev/null
+++ b/application/views/errors/cli/error_db.php
@@ -0,0 +1,8 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+echo "\nDatabase error: ",
+	$heading,
+	"\n\n",
+	$message,
+	"\n\n";
\ No newline at end of file
diff --git a/application/views/errors/cli/error_exception.php b/application/views/errors/cli/error_exception.php
new file mode 100644
index 0000000..efa6a66
--- /dev/null
+++ b/application/views/errors/cli/error_exception.php
@@ -0,0 +1,21 @@
+<?php defined('BASEPATH') OR exit('No direct script access allowed'); ?>
+
+An uncaught Exception was encountered
+
+Type:        <?php echo get_class($exception), "\n"; ?>
+Message:     <?php echo $message, "\n"; ?>
+Filename:    <?php echo $exception->getFile(), "\n"; ?>
+Line Number: <?php echo $exception->getLine(); ?>
+
+<?php if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE === TRUE): ?>
+
+Backtrace:
+<?php	foreach ($exception->getTrace() as $error): ?>
+<?php		if (isset($error['file']) && strpos($error['file'], realpath(BASEPATH)) !== 0): ?>
+	File: <?php echo $error['file'], "\n"; ?>
+	Line: <?php echo $error['line'], "\n"; ?>
+	Function: <?php echo $error['function'], "\n\n"; ?>
+<?php		endif ?>
+<?php	endforeach ?>
+
+<?php endif ?>
diff --git a/application/views/errors/cli/error_general.php b/application/views/errors/cli/error_general.php
new file mode 100644
index 0000000..6984b61
--- /dev/null
+++ b/application/views/errors/cli/error_general.php
@@ -0,0 +1,8 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+echo "\nERROR: ",
+	$heading,
+	"\n\n",
+	$message,
+	"\n\n";
\ No newline at end of file
diff --git a/application/views/errors/cli/error_php.php b/application/views/errors/cli/error_php.php
new file mode 100644
index 0000000..8a24b64
--- /dev/null
+++ b/application/views/errors/cli/error_php.php
@@ -0,0 +1,21 @@
+<?php defined('BASEPATH') OR exit('No direct script access allowed'); ?>
+
+A PHP Error was encountered
+
+Severity:    <?php echo $severity, "\n"; ?>
+Message:     <?php echo $message, "\n"; ?>
+Filename:    <?php echo $filepath, "\n"; ?>
+Line Number: <?php echo $line; ?>
+
+<?php if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE === TRUE): ?>
+
+Backtrace:
+<?php	foreach (debug_backtrace() as $error): ?>
+<?php		if (isset($error['file']) && strpos($error['file'], realpath(BASEPATH)) !== 0): ?>
+	File: <?php echo $error['file'], "\n"; ?>
+	Line: <?php echo $error['line'], "\n"; ?>
+	Function: <?php echo $error['function'], "\n\n"; ?>
+<?php		endif ?>
+<?php	endforeach ?>
+
+<?php endif ?>
diff --git a/application/views/errors/cli/index.html b/application/views/errors/cli/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/views/errors/cli/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/application/views/errors/html/error_404.php b/application/views/errors/html/error_404.php
new file mode 100644
index 0000000..756ea9d
--- /dev/null
+++ b/application/views/errors/html/error_404.php
@@ -0,0 +1,64 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+?><!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>404 Page Not Found</title>
+<style type="text/css">
+
+::selection { background-color: #E13300; color: white; }
+::-moz-selection { background-color: #E13300; color: white; }
+
+body {
+	background-color: #fff;
+	margin: 40px;
+	font: 13px/20px normal Helvetica, Arial, sans-serif;
+	color: #4F5155;
+}
+
+a {
+	color: #003399;
+	background-color: transparent;
+	font-weight: normal;
+}
+
+h1 {
+	color: #444;
+	background-color: transparent;
+	border-bottom: 1px solid #D0D0D0;
+	font-size: 19px;
+	font-weight: normal;
+	margin: 0 0 14px 0;
+	padding: 14px 15px 10px 15px;
+}
+
+code {
+	font-family: Consolas, Monaco, Courier New, Courier, monospace;
+	font-size: 12px;
+	background-color: #f9f9f9;
+	border: 1px solid #D0D0D0;
+	color: #002166;
+	display: block;
+	margin: 14px 0 14px 0;
+	padding: 12px 10px 12px 10px;
+}
+
+#container {
+	margin: 10px;
+	border: 1px solid #D0D0D0;
+	box-shadow: 0 0 8px #D0D0D0;
+}
+
+p {
+	margin: 12px 15px 12px 15px;
+}
+</style>
+</head>
+<body>
+	<div id="container">
+		<h1><?php echo $heading; ?></h1>
+		<?php echo $message; ?>
+	</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/application/views/errors/html/error_db.php b/application/views/errors/html/error_db.php
new file mode 100644
index 0000000..f5a43f6
--- /dev/null
+++ b/application/views/errors/html/error_db.php
@@ -0,0 +1,64 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+?><!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Database Error</title>
+<style type="text/css">
+
+::selection { background-color: #E13300; color: white; }
+::-moz-selection { background-color: #E13300; color: white; }
+
+body {
+	background-color: #fff;
+	margin: 40px;
+	font: 13px/20px normal Helvetica, Arial, sans-serif;
+	color: #4F5155;
+}
+
+a {
+	color: #003399;
+	background-color: transparent;
+	font-weight: normal;
+}
+
+h1 {
+	color: #444;
+	background-color: transparent;
+	border-bottom: 1px solid #D0D0D0;
+	font-size: 19px;
+	font-weight: normal;
+	margin: 0 0 14px 0;
+	padding: 14px 15px 10px 15px;
+}
+
+code {
+	font-family: Consolas, Monaco, Courier New, Courier, monospace;
+	font-size: 12px;
+	background-color: #f9f9f9;
+	border: 1px solid #D0D0D0;
+	color: #002166;
+	display: block;
+	margin: 14px 0 14px 0;
+	padding: 12px 10px 12px 10px;
+}
+
+#container {
+	margin: 10px;
+	border: 1px solid #D0D0D0;
+	box-shadow: 0 0 8px #D0D0D0;
+}
+
+p {
+	margin: 12px 15px 12px 15px;
+}
+</style>
+</head>
+<body>
+	<div id="container">
+		<h1><?php echo $heading; ?></h1>
+		<?php echo $message; ?>
+	</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/application/views/errors/html/error_exception.php b/application/views/errors/html/error_exception.php
new file mode 100644
index 0000000..8784886
--- /dev/null
+++ b/application/views/errors/html/error_exception.php
@@ -0,0 +1,32 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+?>
+
+<div style="border:1px solid #990000;padding-left:20px;margin:0 0 10px 0;">
+
+<h4>An uncaught Exception was encountered</h4>
+
+<p>Type: <?php echo get_class($exception); ?></p>
+<p>Message: <?php echo $message; ?></p>
+<p>Filename: <?php echo $exception->getFile(); ?></p>
+<p>Line Number: <?php echo $exception->getLine(); ?></p>
+
+<?php if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE === TRUE): ?>
+
+	<p>Backtrace:</p>
+	<?php foreach ($exception->getTrace() as $error): ?>
+
+		<?php if (isset($error['file']) && strpos($error['file'], realpath(BASEPATH)) !== 0): ?>
+
+			<p style="margin-left:10px">
+			File: <?php echo $error['file']; ?><br />
+			Line: <?php echo $error['line']; ?><br />
+			Function: <?php echo $error['function']; ?>
+			</p>
+		<?php endif ?>
+
+	<?php endforeach ?>
+
+<?php endif ?>
+
+</div>
\ No newline at end of file
diff --git a/application/views/errors/html/error_general.php b/application/views/errors/html/error_general.php
new file mode 100644
index 0000000..fc3b2eb
--- /dev/null
+++ b/application/views/errors/html/error_general.php
@@ -0,0 +1,64 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+?><!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Error</title>
+<style type="text/css">
+
+::selection { background-color: #E13300; color: white; }
+::-moz-selection { background-color: #E13300; color: white; }
+
+body {
+	background-color: #fff;
+	margin: 40px;
+	font: 13px/20px normal Helvetica, Arial, sans-serif;
+	color: #4F5155;
+}
+
+a {
+	color: #003399;
+	background-color: transparent;
+	font-weight: normal;
+}
+
+h1 {
+	color: #444;
+	background-color: transparent;
+	border-bottom: 1px solid #D0D0D0;
+	font-size: 19px;
+	font-weight: normal;
+	margin: 0 0 14px 0;
+	padding: 14px 15px 10px 15px;
+}
+
+code {
+	font-family: Consolas, Monaco, Courier New, Courier, monospace;
+	font-size: 12px;
+	background-color: #f9f9f9;
+	border: 1px solid #D0D0D0;
+	color: #002166;
+	display: block;
+	margin: 14px 0 14px 0;
+	padding: 12px 10px 12px 10px;
+}
+
+#container {
+	margin: 10px;
+	border: 1px solid #D0D0D0;
+	box-shadow: 0 0 8px #D0D0D0;
+}
+
+p {
+	margin: 12px 15px 12px 15px;
+}
+</style>
+</head>
+<body>
+	<div id="container">
+		<h1><?php echo $heading; ?></h1>
+		<?php echo $message; ?>
+	</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/application/views/errors/html/error_php.php b/application/views/errors/html/error_php.php
new file mode 100644
index 0000000..b146f9c
--- /dev/null
+++ b/application/views/errors/html/error_php.php
@@ -0,0 +1,33 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+?>
+
+<div style="border:1px solid #990000;padding-left:20px;margin:0 0 10px 0;">
+
+<h4>A PHP Error was encountered</h4>
+
+<p>Severity: <?php echo $severity; ?></p>
+<p>Message:  <?php echo $message; ?></p>
+<p>Filename: <?php echo $filepath; ?></p>
+<p>Line Number: <?php echo $line; ?></p>
+
+<?php if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE === TRUE): ?>
+
+	<p>Backtrace:</p>
+	<?php foreach (debug_backtrace() as $error): ?>
+
+		<?php if (isset($error['file']) && strpos($error['file'], realpath(BASEPATH)) !== 0): ?>
+
+			<p style="margin-left:10px">
+			File: <?php echo $error['file'] ?><br />
+			Line: <?php echo $error['line'] ?><br />
+			Function: <?php echo $error['function'] ?>
+			</p>
+
+		<?php endif ?>
+
+	<?php endforeach ?>
+
+<?php endif ?>
+
+</div>
\ No newline at end of file
diff --git a/application/views/errors/html/index.html b/application/views/errors/html/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/views/errors/html/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/application/views/errors/index.html b/application/views/errors/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/views/errors/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/application/views/index.html b/application/views/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/application/views/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/application/views/welcome_message.php b/application/views/welcome_message.php
new file mode 100644
index 0000000..9db22bc
--- /dev/null
+++ b/application/views/welcome_message.php
@@ -0,0 +1,100 @@
+<?php
+defined('BASEPATH') OR exit('No direct script access allowed');
+?><!DOCTYPE html>
+<html lang="en">
+<head>
+	<meta charset="utf-8">
+	<title>Welcome to CodeIgniter</title>
+
+	<style type="text/css">
+
+	::selection { background-color: #E13300; color: white; }
+	::-moz-selection { background-color: #E13300; color: white; }
+
+	body {
+		background-color: #fff;
+		margin: 40px;
+		font: 13px/20px normal Helvetica, Arial, sans-serif;
+		color: #4F5155;
+	}
+
+	a {
+		color: #003399;
+		background-color: transparent;
+		font-weight: normal;
+		text-decoration: none;
+	}
+
+	a:hover {
+		color: #97310e;
+	}
+
+	h1 {
+		color: #444;
+		background-color: transparent;
+		border-bottom: 1px solid #D0D0D0;
+		font-size: 19px;
+		font-weight: normal;
+		margin: 0 0 14px 0;
+		padding: 14px 15px 10px 15px;
+	}
+
+	code {
+		font-family: Consolas, Monaco, Courier New, Courier, monospace;
+		font-size: 12px;
+		background-color: #f9f9f9;
+		border: 1px solid #D0D0D0;
+		color: #002166;
+		display: block;
+		margin: 14px 0 14px 0;
+		padding: 12px 10px 12px 10px;
+	}
+
+	#body {
+		margin: 0 15px 0 15px;
+		min-height: 96px;
+	}
+
+	p {
+		margin: 0 0 10px;
+		padding:0;
+	}
+
+	p.footer {
+		text-align: right;
+		font-size: 11px;
+		border-top: 1px solid #D0D0D0;
+		line-height: 32px;
+		padding: 0 10px 0 10px;
+		margin: 20px 0 0 0;
+	}
+
+	#container {
+		margin: 10px;
+		border: 1px solid #D0D0D0;
+		box-shadow: 0 0 8px #D0D0D0;
+	}
+	</style>
+</head>
+<body>
+
+<div id="container">
+	<h1>Welcome to CodeIgniter!</h1>
+
+	<div id="body">
+		<p>The page you are looking at is being generated dynamically by CodeIgniter.</p>
+
+		<p>If you would like to edit this page you'll find it located at:</p>
+		<code>application/views/welcome_message.php</code>
+
+		<p>The corresponding controller for this page is found at:</p>
+		<code>application/controllers/Welcome.php</code>
+
+		<p>If you are exploring CodeIgniter for the very first time, you should start by reading the <a href="userguide3/">User Guide</a>.</p>
+	</div>
+
+	<p class="footer">Page rendered in <strong>{elapsed_time}</strong> seconds. <?php echo  (ENVIRONMENT === 'development') ?  'CodeIgniter Version <strong>' . CI_VERSION . '</strong>' : '' ?></p>
+</div>
+
+</body>
+</html>
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..01e65f4
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,35 @@
+{
+	"description": "The CodeIgniter framework",
+	"name": "codeigniter/framework",
+	"type": "project",
+	"homepage": "https://codeigniter.com",
+	"license": "MIT",
+	"support": {
+		"forum": "http://forum.codeigniter.com/",
+		"wiki": "https://github.com/bcit-ci/CodeIgniter/wiki",
+		"slack": "https://codeigniterchat.slack.com",
+		"source": "https://github.com/bcit-ci/CodeIgniter"
+	},
+	"require": {
+		"php": ">=5.3.7"
+	},
+	"suggest": {
+		"paragonie/random_compat": "Provides better randomness in PHP 5.x"
+	},
+	"scripts": {
+		"test:coverage": [
+			"@putenv XDEBUG_MODE=coverage",
+			"phpunit --color=always --coverage-text --configuration tests/travis/sqlite.phpunit.xml"
+		],
+		"post-install-cmd": [
+			"sed -i s/name{0}/name[0]/ vendor/mikey179/vfsstream/src/main/php/org/bovigo/vfs/vfsStream.php"
+		],
+		"post-update-cmd": [
+			"sed -i s/name{0}/name[0]/ vendor/mikey179/vfsstream/src/main/php/org/bovigo/vfs/vfsStream.php"
+		]
+	},
+	"require-dev": {
+		"mikey179/vfsstream": "1.6.*",
+		"phpunit/phpunit": "4.* || 5.* || 9.*"
+	}
+}
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..11f8c62
--- /dev/null
+++ b/index.php
@@ -0,0 +1,315 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2014 - 2019, 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+
+/*
+ *---------------------------------------------------------------
+ * APPLICATION ENVIRONMENT
+ *---------------------------------------------------------------
+ *
+ * You can load different configurations depending on your
+ * current environment. Setting the environment also influences
+ * things like logging and error reporting.
+ *
+ * This can be set to anything, but default usage is:
+ *
+ *     development
+ *     testing
+ *     production
+ *
+ * NOTE: If you change these, also change the error_reporting() code below
+ */
+	define('ENVIRONMENT', isset($_SERVER['CI_ENV']) ? $_SERVER['CI_ENV'] : 'development');
+
+/*
+ *---------------------------------------------------------------
+ * ERROR REPORTING
+ *---------------------------------------------------------------
+ *
+ * Different environments will require different levels of error reporting.
+ * By default development will show errors but testing and live will hide them.
+ */
+switch (ENVIRONMENT)
+{
+	case 'development':
+		error_reporting(-1);
+		ini_set('display_errors', 1);
+	break;
+
+	case 'testing':
+	case 'production':
+		ini_set('display_errors', 0);
+		if (version_compare(PHP_VERSION, '5.3', '>='))
+		{
+			error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED);
+		}
+		else
+		{
+			error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_USER_NOTICE);
+		}
+	break;
+
+	default:
+		header('HTTP/1.1 503 Service Unavailable.', TRUE, 503);
+		echo 'The application environment is not set correctly.';
+		exit(1); // EXIT_ERROR
+}
+
+/*
+ *---------------------------------------------------------------
+ * SYSTEM DIRECTORY NAME
+ *---------------------------------------------------------------
+ *
+ * This variable must contain the name of your "system" directory.
+ * Set the path if it is not in the same directory as this file.
+ */
+	$system_path = 'system';
+
+/*
+ *---------------------------------------------------------------
+ * APPLICATION DIRECTORY NAME
+ *---------------------------------------------------------------
+ *
+ * If you want this front controller to use a different "application"
+ * directory than the default one you can set its name here. The directory
+ * can also be renamed or relocated anywhere on your server. If you do,
+ * use an absolute (full) server path.
+ * For more info please see the user guide:
+ *
+ * https://codeigniter.com/userguide3/general/managing_apps.html
+ *
+ * NO TRAILING SLASH!
+ */
+	$application_folder = 'application';
+
+/*
+ *---------------------------------------------------------------
+ * VIEW DIRECTORY NAME
+ *---------------------------------------------------------------
+ *
+ * If you want to move the view directory out of the application
+ * directory, set the path to it here. The directory can be renamed
+ * and relocated anywhere on your server. If blank, it will default
+ * to the standard location inside your application directory.
+ * If you do move this, use an absolute (full) server path.
+ *
+ * NO TRAILING SLASH!
+ */
+	$view_folder = '';
+
+
+/*
+ * --------------------------------------------------------------------
+ * DEFAULT CONTROLLER
+ * --------------------------------------------------------------------
+ *
+ * Normally you will set your default controller in the routes.php file.
+ * You can, however, force a custom routing by hard-coding a
+ * specific controller class/function here. For most applications, you
+ * WILL NOT set your routing here, but it's an option for those
+ * special instances where you might want to override the standard
+ * routing in a specific front controller that shares a common CI installation.
+ *
+ * IMPORTANT: If you set the routing here, NO OTHER controller will be
+ * callable. In essence, this preference limits your application to ONE
+ * specific controller. Leave the function name blank if you need
+ * to call functions dynamically via the URI.
+ *
+ * Un-comment the $routing array below to use this feature
+ */
+	// The directory name, relative to the "controllers" directory.  Leave blank
+	// if your controller is not in a sub-directory within the "controllers" one
+	// $routing['directory'] = '';
+
+	// The controller class file name.  Example:  mycontroller
+	// $routing['controller'] = '';
+
+	// The controller function you wish to be called.
+	// $routing['function']	= '';
+
+
+/*
+ * -------------------------------------------------------------------
+ *  CUSTOM CONFIG VALUES
+ * -------------------------------------------------------------------
+ *
+ * The $assign_to_config array below will be passed dynamically to the
+ * config class when initialized. This allows you to set custom config
+ * items or override any default config values found in the config.php file.
+ * This can be handy as it permits you to share one application between
+ * multiple front controller files, with each file containing different
+ * config values.
+ *
+ * Un-comment the $assign_to_config array below to use this feature
+ */
+	// $assign_to_config['name_of_config_item'] = 'value of config item';
+
+
+
+// --------------------------------------------------------------------
+// END OF USER CONFIGURABLE SETTINGS.  DO NOT EDIT BELOW THIS LINE
+// --------------------------------------------------------------------
+
+/*
+ * ---------------------------------------------------------------
+ *  Resolve the system path for increased reliability
+ * ---------------------------------------------------------------
+ */
+
+	// Set the current directory correctly for CLI requests
+	if (defined('STDIN'))
+	{
+		chdir(dirname(__FILE__));
+	}
+
+	if (($_temp = realpath($system_path)) !== FALSE)
+	{
+		$system_path = $_temp.DIRECTORY_SEPARATOR;
+	}
+	else
+	{
+		// Ensure there's a trailing slash
+		$system_path = strtr(
+			rtrim($system_path, '/\\'),
+			'/\\',
+			DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR
+		).DIRECTORY_SEPARATOR;
+	}
+
+	// Is the system path correct?
+	if ( ! is_dir($system_path))
+	{
+		header('HTTP/1.1 503 Service Unavailable.', TRUE, 503);
+		echo 'Your system folder path does not appear to be set correctly. Please open the following file and correct this: '.pathinfo(__FILE__, PATHINFO_BASENAME);
+		exit(3); // EXIT_CONFIG
+	}
+
+/*
+ * -------------------------------------------------------------------
+ *  Now that we know the path, set the main path constants
+ * -------------------------------------------------------------------
+ */
+	// The name of THIS file
+	define('SELF', pathinfo(__FILE__, PATHINFO_BASENAME));
+
+	// Path to the system directory
+	define('BASEPATH', $system_path);
+
+	// Path to the front controller (this file) directory
+	define('FCPATH', dirname(__FILE__).DIRECTORY_SEPARATOR);
+
+	// Name of the "system" directory
+	define('SYSDIR', basename(BASEPATH));
+
+	// The path to the "application" directory
+	if (is_dir($application_folder))
+	{
+		if (($_temp = realpath($application_folder)) !== FALSE)
+		{
+			$application_folder = $_temp;
+		}
+		else
+		{
+			$application_folder = strtr(
+				rtrim($application_folder, '/\\'),
+				'/\\',
+				DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR
+			);
+		}
+	}
+	elseif (is_dir(BASEPATH.$application_folder.DIRECTORY_SEPARATOR))
+	{
+		$application_folder = BASEPATH.strtr(
+			trim($application_folder, '/\\'),
+			'/\\',
+			DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR
+		);
+	}
+	else
+	{
+		header('HTTP/1.1 503 Service Unavailable.', TRUE, 503);
+		echo 'Your application folder path does not appear to be set correctly. Please open the following file and correct this: '.SELF;
+		exit(3); // EXIT_CONFIG
+	}
+
+	define('APPPATH', $application_folder.DIRECTORY_SEPARATOR);
+
+	// The path to the "views" directory
+	if ( ! isset($view_folder[0]) && is_dir(APPPATH.'views'.DIRECTORY_SEPARATOR))
+	{
+		$view_folder = APPPATH.'views';
+	}
+	elseif (is_dir($view_folder))
+	{
+		if (($_temp = realpath($view_folder)) !== FALSE)
+		{
+			$view_folder = $_temp;
+		}
+		else
+		{
+			$view_folder = strtr(
+				rtrim($view_folder, '/\\'),
+				'/\\',
+				DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR
+			);
+		}
+	}
+	elseif (is_dir(APPPATH.$view_folder.DIRECTORY_SEPARATOR))
+	{
+		$view_folder = APPPATH.strtr(
+			trim($view_folder, '/\\'),
+			'/\\',
+			DIRECTORY_SEPARATOR.DIRECTORY_SEPARATOR
+		);
+	}
+	else
+	{
+		header('HTTP/1.1 503 Service Unavailable.', TRUE, 503);
+		echo 'Your view folder path does not appear to be set correctly. Please open the following file and correct this: '.SELF;
+		exit(3); // EXIT_CONFIG
+	}
+
+	define('VIEWPATH', $view_folder.DIRECTORY_SEPARATOR);
+
+/*
+ * --------------------------------------------------------------------
+ * LOAD THE BOOTSTRAP FILE
+ * --------------------------------------------------------------------
+ *
+ * And away we go...
+ */
+require_once BASEPATH.'core/CodeIgniter.php';
diff --git a/readme.rst b/readme.rst
new file mode 100644
index 0000000..63a55c3
--- /dev/null
+++ b/readme.rst
@@ -0,0 +1,71 @@
+###################
+What is CodeIgniter
+###################
+
+CodeIgniter is an Application Development Framework - a toolkit - for people
+who build web sites using PHP. Its goal is to enable you to develop projects
+much faster than you could if you were writing code from scratch, by providing
+a rich set of libraries for commonly needed tasks, as well as a simple
+interface and logical structure to access these libraries. CodeIgniter lets
+you creatively focus on your project by minimizing the amount of code needed
+for a given task.
+
+*******************
+Release Information
+*******************
+
+This repo contains in-development code for future releases. To download the
+latest stable release please visit the `CodeIgniter Downloads
+<https://codeigniter.com/download>`_ page.
+
+**************************
+Changelog and New Features
+**************************
+
+You can find a list of all changes for each release in the `user
+guide change log <https://github.com/bcit-ci/CodeIgniter/blob/develop/user_guide_src/source/changelog.rst>`_.
+
+*******************
+Server Requirements
+*******************
+
+PHP version 5.6 or newer is recommended.
+
+It should work on 5.3.7 as well, but we strongly advise you NOT to run
+such old versions of PHP, because of potential security and performance
+issues, as well as missing features.
+
+************
+Installation
+************
+
+Please see the `installation section <https://codeigniter.com/userguide3/installation/index.html>`_
+of the CodeIgniter User Guide.
+
+*******
+License
+*******
+
+Please see the `license
+agreement <https://github.com/bcit-ci/CodeIgniter/blob/develop/user_guide_src/source/license.rst>`_.
+
+*********
+Resources
+*********
+
+-  `User Guide <https://codeigniter.com/docs>`_
+-  `Contributing Guide <https://github.com/bcit-ci/CodeIgniter/blob/develop/contributing.md>`_
+-  `Language File Translations <https://github.com/bcit-ci/codeigniter3-translations>`_
+-  `Community Forums <http://forum.codeigniter.com/>`_
+-  `Community Wiki <https://github.com/bcit-ci/CodeIgniter/wiki>`_
+-  `Community Slack Channel <https://codeigniterchat.slack.com>`_
+
+Report security issues to our `Security Panel <mailto:security@codeigniter.com>`_
+or via our `page on HackerOne <https://hackerone.com/codeigniter>`_, thank you.
+
+***************
+Acknowledgement
+***************
+
+The CodeIgniter team would like to thank EllisLab, all the
+contributors to the CodeIgniter project and you, the CodeIgniter user.
diff --git a/system/.htaccess b/system/.htaccess
new file mode 100644
index 0000000..97c65d2
--- /dev/null
+++ b/system/.htaccess
@@ -0,0 +1,6 @@
+<IfModule authz_core_module>
+	Require all denied
+</IfModule>
+<IfModule !authz_core_module>
+	Deny from all
+</IfModule>
\ No newline at end of file
diff --git a/system/core/Benchmark.php b/system/core/Benchmark.php
new file mode 100644
index 0000000..20ac2f5
--- /dev/null
+++ b/system/core/Benchmark.php
@@ -0,0 +1,134 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Benchmark Class
+ *
+ * This class enables you to mark points and calculate the time difference
+ * between them. Memory consumption can also be displayed.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/libraries/benchmark.html
+ */
+class CI_Benchmark {
+
+	/**
+	 * List of all benchmark markers
+	 *
+	 * @var	array
+	 */
+	public $marker = array();
+
+	/**
+	 * Set a benchmark marker
+	 *
+	 * Multiple calls to this function can be made so that several
+	 * execution points can be timed.
+	 *
+	 * @param	string	$name	Marker name
+	 * @return	void
+	 */
+	public function mark($name)
+	{
+		$this->marker[$name] = microtime(TRUE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Elapsed time
+	 *
+	 * Calculates the time difference between two marked points.
+	 *
+	 * If the first parameter is empty this function instead returns the
+	 * {elapsed_time} pseudo-variable. This permits the full system
+	 * execution time to be shown in a template. The output class will
+	 * swap the real value for this variable.
+	 *
+	 * @param	string	$point1		A particular marked point
+	 * @param	string	$point2		A particular marked point
+	 * @param	int	$decimals	Number of decimal places
+	 *
+	 * @return	string	Calculated elapsed time on success,
+	 *			an '{elapsed_string}' if $point1 is empty
+	 *			or an empty string if $point1 is not found.
+	 */
+	public function elapsed_time($point1 = '', $point2 = '', $decimals = 4)
+	{
+		if ($point1 === '')
+		{
+			return '{elapsed_time}';
+		}
+
+		if ( ! isset($this->marker[$point1]))
+		{
+			return '';
+		}
+
+		if ( ! isset($this->marker[$point2]))
+		{
+			$this->marker[$point2] = microtime(TRUE);
+		}
+
+		return number_format($this->marker[$point2] - $this->marker[$point1], $decimals);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Memory Usage
+	 *
+	 * Simply returns the {memory_usage} marker.
+	 *
+	 * This permits it to be put it anywhere in a template
+	 * without the memory being calculated until the end.
+	 * The output class will swap the real value for this variable.
+	 *
+	 * @return	string	'{memory_usage}'
+	 */
+	public function memory_usage()
+	{
+		return '{memory_usage}';
+	}
+
+}
diff --git a/system/core/CodeIgniter.php b/system/core/CodeIgniter.php
new file mode 100644
index 0000000..56826dc
--- /dev/null
+++ b/system/core/CodeIgniter.php
@@ -0,0 +1,560 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * System Initialization File
+ *
+ * Loads the base classes and executes the request.
+ *
+ * @package		CodeIgniter
+ * @subpackage	CodeIgniter
+ * @category	Front-controller
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/
+ */
+
+/**
+ * CodeIgniter Version
+ *
+ * @var	string
+ *
+ */
+	const CI_VERSION = '3.1.13';
+
+/*
+ * ------------------------------------------------------
+ *  Load the framework constants
+ * ------------------------------------------------------
+ */
+	if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/constants.php'))
+	{
+		require_once(APPPATH.'config/'.ENVIRONMENT.'/constants.php');
+	}
+
+	if (file_exists(APPPATH.'config/constants.php'))
+	{
+		require_once(APPPATH.'config/constants.php');
+	}
+
+/*
+ * ------------------------------------------------------
+ *  Load the global functions
+ * ------------------------------------------------------
+ */
+	require_once(BASEPATH.'core/Common.php');
+
+
+/*
+ * ------------------------------------------------------
+ * Security procedures
+ * ------------------------------------------------------
+ */
+
+if ( ! is_php('5.4'))
+{
+	ini_set('magic_quotes_runtime', 0);
+
+	if ((bool) ini_get('register_globals'))
+	{
+		$_protected = array(
+			'_SERVER',
+			'_GET',
+			'_POST',
+			'_FILES',
+			'_REQUEST',
+			'_SESSION',
+			'_ENV',
+			'_COOKIE',
+			'GLOBALS',
+			'HTTP_RAW_POST_DATA',
+			'system_path',
+			'application_folder',
+			'view_folder',
+			'_protected',
+			'_registered'
+		);
+
+		$_registered = ini_get('variables_order');
+		foreach (array('E' => '_ENV', 'G' => '_GET', 'P' => '_POST', 'C' => '_COOKIE', 'S' => '_SERVER') as $key => $superglobal)
+		{
+			if (strpos($_registered, $key) === FALSE)
+			{
+				continue;
+			}
+
+			foreach (array_keys($$superglobal) as $var)
+			{
+				if (isset($GLOBALS[$var]) && ! in_array($var, $_protected, TRUE))
+				{
+					$GLOBALS[$var] = NULL;
+				}
+			}
+		}
+	}
+}
+
+
+/*
+ * ------------------------------------------------------
+ *  Define a custom error handler so we can log PHP errors
+ * ------------------------------------------------------
+ */
+	set_error_handler('_error_handler');
+	set_exception_handler('_exception_handler');
+	register_shutdown_function('_shutdown_handler');
+
+/*
+ * ------------------------------------------------------
+ *  Set the subclass_prefix
+ * ------------------------------------------------------
+ *
+ * Normally the "subclass_prefix" is set in the config file.
+ * The subclass prefix allows CI to know if a core class is
+ * being extended via a library in the local application
+ * "libraries" folder. Since CI allows config items to be
+ * overridden via data set in the main index.php file,
+ * before proceeding we need to know if a subclass_prefix
+ * override exists. If so, we will set this value now,
+ * before any classes are loaded
+ * Note: Since the config file data is cached it doesn't
+ * hurt to load it here.
+ */
+	if ( ! empty($assign_to_config['subclass_prefix']))
+	{
+		get_config(array('subclass_prefix' => $assign_to_config['subclass_prefix']));
+	}
+
+/*
+ * ------------------------------------------------------
+ *  Should we use a Composer autoloader?
+ * ------------------------------------------------------
+ */
+	if ($composer_autoload = config_item('composer_autoload'))
+	{
+		if ($composer_autoload === TRUE)
+		{
+			file_exists(APPPATH.'vendor/autoload.php')
+				? require_once(APPPATH.'vendor/autoload.php')
+				: log_message('error', '$config[\'composer_autoload\'] is set to TRUE but '.APPPATH.'vendor/autoload.php was not found.');
+		}
+		elseif (file_exists($composer_autoload))
+		{
+			require_once($composer_autoload);
+		}
+		else
+		{
+			log_message('error', 'Could not find the specified $config[\'composer_autoload\'] path: '.$composer_autoload);
+		}
+	}
+
+/*
+ * ------------------------------------------------------
+ *  Start the timer... tick tock tick tock...
+ * ------------------------------------------------------
+ */
+	$BM =& load_class('Benchmark', 'core');
+	$BM->mark('total_execution_time_start');
+	$BM->mark('loading_time:_base_classes_start');
+
+/*
+ * ------------------------------------------------------
+ *  Instantiate the hooks class
+ * ------------------------------------------------------
+ */
+	$EXT =& load_class('Hooks', 'core');
+
+/*
+ * ------------------------------------------------------
+ *  Is there a "pre_system" hook?
+ * ------------------------------------------------------
+ */
+	$EXT->call_hook('pre_system');
+
+/*
+ * ------------------------------------------------------
+ *  Instantiate the config class
+ * ------------------------------------------------------
+ *
+ * Note: It is important that Config is loaded first as
+ * most other classes depend on it either directly or by
+ * depending on another class that uses it.
+ *
+ */
+	$CFG =& load_class('Config', 'core');
+
+	// Do we have any manually set config items in the index.php file?
+	if (isset($assign_to_config) && is_array($assign_to_config))
+	{
+		foreach ($assign_to_config as $key => $value)
+		{
+			$CFG->set_item($key, $value);
+		}
+	}
+
+/*
+ * ------------------------------------------------------
+ * Important charset-related stuff
+ * ------------------------------------------------------
+ *
+ * Configure mbstring and/or iconv if they are enabled
+ * and set MB_ENABLED and ICONV_ENABLED constants, so
+ * that we don't repeatedly do extension_loaded() or
+ * function_exists() calls.
+ *
+ * Note: UTF-8 class depends on this. It used to be done
+ * in it's constructor, but it's _not_ class-specific.
+ *
+ */
+	$charset = strtoupper(config_item('charset'));
+	ini_set('default_charset', $charset);
+
+	if (extension_loaded('mbstring'))
+	{
+		define('MB_ENABLED', TRUE);
+		// mbstring.internal_encoding is deprecated starting with PHP 5.6
+		// and it's usage triggers E_DEPRECATED messages.
+		@ini_set('mbstring.internal_encoding', $charset);
+		// This is required for mb_convert_encoding() to strip invalid characters.
+		// That's utilized by CI_Utf8, but it's also done for consistency with iconv.
+		mb_substitute_character('none');
+	}
+	else
+	{
+		define('MB_ENABLED', FALSE);
+	}
+
+	// There's an ICONV_IMPL constant, but the PHP manual says that using
+	// iconv's predefined constants is "strongly discouraged".
+	if (extension_loaded('iconv'))
+	{
+		define('ICONV_ENABLED', TRUE);
+		// iconv.internal_encoding is deprecated starting with PHP 5.6
+		// and it's usage triggers E_DEPRECATED messages.
+		@ini_set('iconv.internal_encoding', $charset);
+	}
+	else
+	{
+		define('ICONV_ENABLED', FALSE);
+	}
+
+	if (is_php('5.6'))
+	{
+		ini_set('php.internal_encoding', $charset);
+	}
+
+/*
+ * ------------------------------------------------------
+ *  Load compatibility features
+ * ------------------------------------------------------
+ */
+
+	require_once(BASEPATH.'core/compat/mbstring.php');
+	require_once(BASEPATH.'core/compat/hash.php');
+	require_once(BASEPATH.'core/compat/password.php');
+	require_once(BASEPATH.'core/compat/standard.php');
+
+/*
+ * ------------------------------------------------------
+ *  Instantiate the UTF-8 class
+ * ------------------------------------------------------
+ */
+	$UNI =& load_class('Utf8', 'core');
+
+/*
+ * ------------------------------------------------------
+ *  Instantiate the URI class
+ * ------------------------------------------------------
+ */
+	$URI =& load_class('URI', 'core');
+
+/*
+ * ------------------------------------------------------
+ *  Instantiate the routing class and set the routing
+ * ------------------------------------------------------
+ */
+	$RTR =& load_class('Router', 'core', isset($routing) ? $routing : NULL);
+
+/*
+ * ------------------------------------------------------
+ *  Instantiate the output class
+ * ------------------------------------------------------
+ */
+	$OUT =& load_class('Output', 'core');
+
+/*
+ * ------------------------------------------------------
+ *	Is there a valid cache file? If so, we're done...
+ * ------------------------------------------------------
+ */
+	if ($EXT->call_hook('cache_override') === FALSE && $OUT->_display_cache($CFG, $URI) === TRUE)
+	{
+		exit;
+	}
+
+/*
+ * -----------------------------------------------------
+ * Load the security class for xss and csrf support
+ * -----------------------------------------------------
+ */
+	$SEC =& load_class('Security', 'core');
+
+/*
+ * ------------------------------------------------------
+ *  Load the Input class and sanitize globals
+ * ------------------------------------------------------
+ */
+	$IN	=& load_class('Input', 'core');
+
+/*
+ * ------------------------------------------------------
+ *  Load the Language class
+ * ------------------------------------------------------
+ */
+	$LANG =& load_class('Lang', 'core');
+
+/*
+ * ------------------------------------------------------
+ *  Load the app controller and local controller
+ * ------------------------------------------------------
+ *
+ */
+	// Load the base controller class
+	require_once BASEPATH.'core/Controller.php';
+
+	/**
+	 * Reference to the CI_Controller method.
+	 *
+	 * Returns current CI instance object
+	 *
+	 * @return CI_Controller
+	 */
+	function &get_instance()
+	{
+		return CI_Controller::get_instance();
+	}
+
+	if (file_exists(APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php'))
+	{
+		require_once APPPATH.'core/'.$CFG->config['subclass_prefix'].'Controller.php';
+	}
+
+	// Set a mark point for benchmarking
+	$BM->mark('loading_time:_base_classes_end');
+
+/*
+ * ------------------------------------------------------
+ *  Sanity checks
+ * ------------------------------------------------------
+ *
+ *  The Router class has already validated the request,
+ *  leaving us with 3 options here:
+ *
+ *	1) an empty class name, if we reached the default
+ *	   controller, but it didn't exist;
+ *	2) a query string which doesn't go through a
+ *	   file_exists() check
+ *	3) a regular request for a non-existing page
+ *
+ *  We handle all of these as a 404 error.
+ *
+ *  Furthermore, none of the methods in the app controller
+ *  or the loader class can be called via the URI, nor can
+ *  controller methods that begin with an underscore.
+ */
+
+	$e404 = FALSE;
+	$class = ucfirst($RTR->class);
+	$method = $RTR->method;
+
+	if (empty($class) OR ! file_exists(APPPATH.'controllers/'.$RTR->directory.$class.'.php'))
+	{
+		$e404 = TRUE;
+	}
+	else
+	{
+		require_once(APPPATH.'controllers/'.$RTR->directory.$class.'.php');
+
+		if ( ! class_exists($class, FALSE) OR $method[0] === '_' OR method_exists('CI_Controller', $method))
+		{
+			$e404 = TRUE;
+		}
+		elseif (method_exists($class, '_remap'))
+		{
+			$params = array($method, array_slice($URI->rsegments, 2));
+			$method = '_remap';
+		}
+		elseif ( ! method_exists($class, $method))
+		{
+			$e404 = TRUE;
+		}
+		/**
+		 * DO NOT CHANGE THIS, NOTHING ELSE WORKS!
+		 *
+		 * - method_exists() returns true for non-public methods, which passes the previous elseif
+		 * - is_callable() returns false for PHP 4-style constructors, even if there's a __construct()
+		 * - method_exists($class, '__construct') won't work because CI_Controller::__construct() is inherited
+		 * - People will only complain if this doesn't work, even though it is documented that it shouldn't.
+		 *
+		 * ReflectionMethod::isConstructor() is the ONLY reliable check,
+		 * knowing which method will be executed as a constructor.
+		 */
+		else
+		{
+			$reflection = new ReflectionMethod($class, $method);
+			if ( ! $reflection->isPublic() OR $reflection->isConstructor())
+			{
+				$e404 = TRUE;
+			}
+		}
+	}
+
+	if ($e404)
+	{
+		if ( ! empty($RTR->routes['404_override']))
+		{
+			if (sscanf($RTR->routes['404_override'], '%[^/]/%s', $error_class, $error_method) !== 2)
+			{
+				$error_method = 'index';
+			}
+
+			$error_class = ucfirst($error_class);
+
+			if ( ! class_exists($error_class, FALSE))
+			{
+				if (file_exists(APPPATH.'controllers/'.$RTR->directory.$error_class.'.php'))
+				{
+					require_once(APPPATH.'controllers/'.$RTR->directory.$error_class.'.php');
+					$e404 = ! class_exists($error_class, FALSE);
+				}
+				// Were we in a directory? If so, check for a global override
+				elseif ( ! empty($RTR->directory) && file_exists(APPPATH.'controllers/'.$error_class.'.php'))
+				{
+					require_once(APPPATH.'controllers/'.$error_class.'.php');
+					if (($e404 = ! class_exists($error_class, FALSE)) === FALSE)
+					{
+						$RTR->directory = '';
+					}
+				}
+			}
+			else
+			{
+				$e404 = FALSE;
+			}
+		}
+
+		// Did we reset the $e404 flag? If so, set the rsegments, starting from index 1
+		if ( ! $e404)
+		{
+			$class = $error_class;
+			$method = $error_method;
+
+			$URI->rsegments = array(
+				1 => $class,
+				2 => $method
+			);
+		}
+		else
+		{
+			show_404($RTR->directory.$class.'/'.$method);
+		}
+	}
+
+	if ($method !== '_remap')
+	{
+		$params = array_slice($URI->rsegments, 2);
+	}
+
+/*
+ * ------------------------------------------------------
+ *  Is there a "pre_controller" hook?
+ * ------------------------------------------------------
+ */
+	$EXT->call_hook('pre_controller');
+
+/*
+ * ------------------------------------------------------
+ *  Instantiate the requested controller
+ * ------------------------------------------------------
+ */
+	// Mark a start point so we can benchmark the controller
+	$BM->mark('controller_execution_time_( '.$class.' / '.$method.' )_start');
+
+	$CI = new $class();
+
+/*
+ * ------------------------------------------------------
+ *  Is there a "post_controller_constructor" hook?
+ * ------------------------------------------------------
+ */
+	$EXT->call_hook('post_controller_constructor');
+
+/*
+ * ------------------------------------------------------
+ *  Call the requested method
+ * ------------------------------------------------------
+ */
+	call_user_func_array(array(&$CI, $method), $params);
+
+	// Mark a benchmark end point
+	$BM->mark('controller_execution_time_( '.$class.' / '.$method.' )_end');
+
+/*
+ * ------------------------------------------------------
+ *  Is there a "post_controller" hook?
+ * ------------------------------------------------------
+ */
+	$EXT->call_hook('post_controller');
+
+/*
+ * ------------------------------------------------------
+ *  Send the final rendered output to the browser
+ * ------------------------------------------------------
+ */
+	if ($EXT->call_hook('display_override') === FALSE)
+	{
+		$OUT->_display();
+	}
+
+/*
+ * ------------------------------------------------------
+ *  Is there a "post_system" hook?
+ * ------------------------------------------------------
+ */
+	$EXT->call_hook('post_system');
diff --git a/system/core/Common.php b/system/core/Common.php
new file mode 100644
index 0000000..a56cb14
--- /dev/null
+++ b/system/core/Common.php
@@ -0,0 +1,849 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Common Functions
+ *
+ * Loads the base classes and executes the request.
+ *
+ * @package		CodeIgniter
+ * @subpackage	CodeIgniter
+ * @category	Common Functions
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('is_php'))
+{
+	/**
+	 * Determines if the current version of PHP is equal to or greater than the supplied value
+	 *
+	 * @param	string
+	 * @return	bool	TRUE if the current version is $version or higher
+	 */
+	function is_php($version)
+	{
+		static $_is_php;
+		$version = (string) $version;
+
+		if ( ! isset($_is_php[$version]))
+		{
+			$_is_php[$version] = version_compare(PHP_VERSION, $version, '>=');
+		}
+
+		return $_is_php[$version];
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('is_really_writable'))
+{
+	/**
+	 * Tests for file writability
+	 *
+	 * is_writable() returns TRUE on Windows servers when you really can't write to
+	 * the file, based on the read-only attribute. is_writable() is also unreliable
+	 * on Unix servers if safe_mode is on.
+	 *
+	 * @link	https://bugs.php.net/bug.php?id=54709
+	 * @param	string
+	 * @return	bool
+	 */
+	function is_really_writable($file)
+	{
+		// If we're on a Unix server with safe_mode off we call is_writable
+		if (DIRECTORY_SEPARATOR === '/' && (is_php('5.4') OR ! ini_get('safe_mode')))
+		{
+			return is_writable($file);
+		}
+
+		/* For Windows servers and safe_mode "on" installations we'll actually
+		 * write a file then read it. Bah...
+		 */
+		if (is_dir($file))
+		{
+			$file = rtrim($file, '/').'/'.md5(mt_rand());
+			if (($fp = @fopen($file, 'ab')) === FALSE)
+			{
+				return FALSE;
+			}
+
+			fclose($fp);
+			@chmod($file, 0777);
+			@unlink($file);
+			return TRUE;
+		}
+		elseif ( ! is_file($file) OR ($fp = @fopen($file, 'ab')) === FALSE)
+		{
+			return FALSE;
+		}
+
+		fclose($fp);
+		return TRUE;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('load_class'))
+{
+	/**
+	 * Class registry
+	 *
+	 * This function acts as a singleton. If the requested class does not
+	 * exist it is instantiated and set to a static variable. If it has
+	 * previously been instantiated the variable is returned.
+	 *
+	 * @param	string	the class name being requested
+	 * @param	string	the directory where the class should be found
+	 * @param	mixed	an optional argument to pass to the class constructor
+	 * @return	object
+	 */
+	function &load_class($class, $directory = 'libraries', $param = NULL)
+	{
+		static $_classes = array();
+
+		// Does the class exist? If so, we're done...
+		if (isset($_classes[$class]))
+		{
+			return $_classes[$class];
+		}
+
+		$name = FALSE;
+
+		// Look for the class first in the local application/libraries folder
+		// then in the native system/libraries folder
+		foreach (array(APPPATH, BASEPATH) as $path)
+		{
+			if (file_exists($path.$directory.'/'.$class.'.php'))
+			{
+				$name = 'CI_'.$class;
+
+				if (class_exists($name, FALSE) === FALSE)
+				{
+					require_once($path.$directory.'/'.$class.'.php');
+				}
+
+				break;
+			}
+		}
+
+		// Is the request a class extension? If so we load it too
+		if (file_exists(APPPATH.$directory.'/'.config_item('subclass_prefix').$class.'.php'))
+		{
+			$name = config_item('subclass_prefix').$class;
+
+			if (class_exists($name, FALSE) === FALSE)
+			{
+				require_once(APPPATH.$directory.'/'.$name.'.php');
+			}
+		}
+
+		// Did we find the class?
+		if ($name === FALSE)
+		{
+			// Note: We use exit() rather than show_error() in order to avoid a
+			// self-referencing loop with the Exceptions class
+			set_status_header(503);
+			echo 'Unable to locate the specified class: '.$class.'.php';
+			exit(5); // EXIT_UNK_CLASS
+		}
+
+		// Keep track of what we just loaded
+		is_loaded($class);
+
+		$_classes[$class] = isset($param)
+			? new $name($param)
+			: new $name();
+		return $_classes[$class];
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('is_loaded'))
+{
+	/**
+	 * Keeps track of which libraries have been loaded. This function is
+	 * called by the load_class() function above
+	 *
+	 * @param	string
+	 * @return	array
+	 */
+	function &is_loaded($class = '')
+	{
+		static $_is_loaded = array();
+
+		if ($class !== '')
+		{
+			$_is_loaded[strtolower($class)] = $class;
+		}
+
+		return $_is_loaded;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('get_config'))
+{
+	/**
+	 * Loads the main config.php file
+	 *
+	 * This function lets us grab the config file even if the Config class
+	 * hasn't been instantiated yet
+	 *
+	 * @param	array
+	 * @return	array
+	 */
+	function &get_config(Array $replace = array())
+	{
+		static $config;
+
+		if (empty($config))
+		{
+			$file_path = APPPATH.'config/config.php';
+			$found = FALSE;
+			if (file_exists($file_path))
+			{
+				$found = TRUE;
+				require($file_path);
+			}
+
+			// Is the config file in the environment folder?
+			if (file_exists($file_path = APPPATH.'config/'.ENVIRONMENT.'/config.php'))
+			{
+				require($file_path);
+			}
+			elseif ( ! $found)
+			{
+				set_status_header(503);
+				echo 'The configuration file does not exist.';
+				exit(3); // EXIT_CONFIG
+			}
+
+			// Does the $config array exist in the file?
+			if ( ! isset($config) OR ! is_array($config))
+			{
+				set_status_header(503);
+				echo 'Your config file does not appear to be formatted correctly.';
+				exit(3); // EXIT_CONFIG
+			}
+		}
+
+		// Are any values being dynamically added or replaced?
+		foreach ($replace as $key => $val)
+		{
+			$config[$key] = $val;
+		}
+
+		return $config;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('config_item'))
+{
+	/**
+	 * Returns the specified config item
+	 *
+	 * @param	string
+	 * @return	mixed
+	 */
+	function config_item($item)
+	{
+		static $_config;
+
+		if (empty($_config))
+		{
+			// references cannot be directly assigned to static variables, so we use an array
+			$_config[0] =& get_config();
+		}
+
+		return isset($_config[0][$item]) ? $_config[0][$item] : NULL;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('get_mimes'))
+{
+	/**
+	 * Returns the MIME types array from config/mimes.php
+	 *
+	 * @return	array
+	 */
+	function &get_mimes()
+	{
+		static $_mimes;
+
+		if (empty($_mimes))
+		{
+			$_mimes = file_exists(APPPATH.'config/mimes.php')
+				? include(APPPATH.'config/mimes.php')
+				: array();
+
+			if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/mimes.php'))
+			{
+				$_mimes = array_merge($_mimes, include(APPPATH.'config/'.ENVIRONMENT.'/mimes.php'));
+			}
+		}
+
+		return $_mimes;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('is_https'))
+{
+	/**
+	 * Is HTTPS?
+	 *
+	 * Determines if the application is accessed via an encrypted
+	 * (HTTPS) connection.
+	 *
+	 * @return	bool
+	 */
+	function is_https()
+	{
+		if ( ! empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off')
+		{
+			return TRUE;
+		}
+		elseif (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https')
+		{
+			return TRUE;
+		}
+		elseif ( ! empty($_SERVER['HTTP_FRONT_END_HTTPS']) && strtolower($_SERVER['HTTP_FRONT_END_HTTPS']) !== 'off')
+		{
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('is_cli'))
+{
+
+	/**
+	 * Is CLI?
+	 *
+	 * Test to see if a request was made from the command line.
+	 *
+	 * @return 	bool
+	 */
+	function is_cli()
+	{
+		return (PHP_SAPI === 'cli' OR defined('STDIN'));
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('show_error'))
+{
+	/**
+	 * Error Handler
+	 *
+	 * This function lets us invoke the exception class and
+	 * display errors using the standard error template located
+	 * in application/views/errors/error_general.php
+	 * This function will send the error page directly to the
+	 * browser and exit.
+	 *
+	 * @param	string
+	 * @param	int
+	 * @param	string
+	 * @return	void
+	 */
+	function show_error($message, $status_code = 500, $heading = 'An Error Was Encountered')
+	{
+		$status_code = abs($status_code);
+		if ($status_code < 100)
+		{
+			$exit_status = $status_code + 9; // 9 is EXIT__AUTO_MIN
+			$status_code = 500;
+		}
+		else
+		{
+			$exit_status = 1; // EXIT_ERROR
+		}
+
+		$_error =& load_class('Exceptions', 'core');
+		echo $_error->show_error($heading, $message, 'error_general', $status_code);
+		exit($exit_status);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('show_404'))
+{
+	/**
+	 * 404 Page Handler
+	 *
+	 * This function is similar to the show_error() function above
+	 * However, instead of the standard error template it displays
+	 * 404 errors.
+	 *
+	 * @param	string
+	 * @param	bool
+	 * @return	void
+	 */
+	function show_404($page = '', $log_error = TRUE)
+	{
+		$_error =& load_class('Exceptions', 'core');
+		$_error->show_404($page, $log_error);
+		exit(4); // EXIT_UNKNOWN_FILE
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('log_message'))
+{
+	/**
+	 * Error Logging Interface
+	 *
+	 * We use this as a simple mechanism to access the logging
+	 * class and send messages to be logged.
+	 *
+	 * @param	string	the error level: 'error', 'debug' or 'info'
+	 * @param	string	the error message
+	 * @return	void
+	 */
+	function log_message($level, $message)
+	{
+		static $_log;
+
+		if ($_log === NULL)
+		{
+			// references cannot be directly assigned to static variables, so we use an array
+			$_log[0] =& load_class('Log', 'core');
+		}
+
+		$_log[0]->write_log($level, $message);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('set_status_header'))
+{
+	/**
+	 * Set HTTP Status Header
+	 *
+	 * @param	int	the status code
+	 * @param	string
+	 * @return	void
+	 */
+	function set_status_header($code = 200, $text = '')
+	{
+		if (is_cli())
+		{
+			return;
+		}
+
+		if (empty($code) OR ! is_numeric($code))
+		{
+			show_error('Status codes must be numeric', 500);
+		}
+
+		if (empty($text))
+		{
+			is_int($code) OR $code = (int) $code;
+			$stati = array(
+				100	=> 'Continue',
+				101	=> 'Switching Protocols',
+
+				200	=> 'OK',
+				201	=> 'Created',
+				202	=> 'Accepted',
+				203	=> 'Non-Authoritative Information',
+				204	=> 'No Content',
+				205	=> 'Reset Content',
+				206	=> 'Partial Content',
+
+				300	=> 'Multiple Choices',
+				301	=> 'Moved Permanently',
+				302	=> 'Found',
+				303	=> 'See Other',
+				304	=> 'Not Modified',
+				305	=> 'Use Proxy',
+				307	=> 'Temporary Redirect',
+
+				400	=> 'Bad Request',
+				401	=> 'Unauthorized',
+				402	=> 'Payment Required',
+				403	=> 'Forbidden',
+				404	=> 'Not Found',
+				405	=> 'Method Not Allowed',
+				406	=> 'Not Acceptable',
+				407	=> 'Proxy Authentication Required',
+				408	=> 'Request Timeout',
+				409	=> 'Conflict',
+				410	=> 'Gone',
+				411	=> 'Length Required',
+				412	=> 'Precondition Failed',
+				413	=> 'Request Entity Too Large',
+				414	=> 'Request-URI Too Long',
+				415	=> 'Unsupported Media Type',
+				416	=> 'Requested Range Not Satisfiable',
+				417	=> 'Expectation Failed',
+				422	=> 'Unprocessable Entity',
+				426	=> 'Upgrade Required',
+				428	=> 'Precondition Required',
+				429	=> 'Too Many Requests',
+				431	=> 'Request Header Fields Too Large',
+
+				500	=> 'Internal Server Error',
+				501	=> 'Not Implemented',
+				502	=> 'Bad Gateway',
+				503	=> 'Service Unavailable',
+				504	=> 'Gateway Timeout',
+				505	=> 'HTTP Version Not Supported',
+				511	=> 'Network Authentication Required',
+			);
+
+			if (isset($stati[$code]))
+			{
+				$text = $stati[$code];
+			}
+			else
+			{
+				show_error('No status text available. Please check your status code number or supply your own message text.', 500);
+			}
+		}
+
+		if (strpos(PHP_SAPI, 'cgi') === 0)
+		{
+			header('Status: '.$code.' '.$text, TRUE);
+			return;
+		}
+
+		$server_protocol = (isset($_SERVER['SERVER_PROTOCOL']) && in_array($_SERVER['SERVER_PROTOCOL'], array('HTTP/1.0', 'HTTP/1.1', 'HTTP/2', 'HTTP/2.0'), TRUE))
+			? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.1';
+		header($server_protocol.' '.$code.' '.$text, TRUE, $code);
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('_error_handler'))
+{
+	/**
+	 * Error Handler
+	 *
+	 * This is the custom error handler that is declared at the (relative)
+	 * top of CodeIgniter.php. The main reason we use this is to permit
+	 * PHP errors to be logged in our own log files since the user may
+	 * not have access to server logs. Since this function effectively
+	 * intercepts PHP errors, however, we also need to display errors
+	 * based on the current error_reporting level.
+	 * We do that with the use of a PHP error template.
+	 *
+	 * @param	int	$severity
+	 * @param	string	$message
+	 * @param	string	$filepath
+	 * @param	int	$line
+	 * @return	void
+	 */
+	function _error_handler($severity, $message, $filepath, $line)
+	{
+		$is_error = (((E_ERROR | E_PARSE | E_COMPILE_ERROR | E_CORE_ERROR | E_USER_ERROR) & $severity) === $severity);
+
+		// When an error occurred, set the status header to '500 Internal Server Error'
+		// to indicate to the client something went wrong.
+		// This can't be done within the $_error->show_php_error method because
+		// it is only called when the display_errors flag is set (which isn't usually
+		// the case in a production environment) or when errors are ignored because
+		// they are above the error_reporting threshold.
+		if ($is_error)
+		{
+			set_status_header(500);
+		}
+
+		// Should we ignore the error? We'll get the current error_reporting
+		// level and add its bits with the severity bits to find out.
+		if (($severity & error_reporting()) !== $severity)
+		{
+			return;
+		}
+
+		$_error =& load_class('Exceptions', 'core');
+		$_error->log_exception($severity, $message, $filepath, $line);
+
+		// Should we display the error?
+		if (str_ireplace(array('off', 'none', 'no', 'false', 'null'), '', ini_get('display_errors')))
+		{
+			$_error->show_php_error($severity, $message, $filepath, $line);
+		}
+
+		// If the error is fatal, the execution of the script should be stopped because
+		// errors can't be recovered from. Halting the script conforms with PHP's
+		// default error handling. See http://www.php.net/manual/en/errorfunc.constants.php
+		if ($is_error)
+		{
+			exit(1); // EXIT_ERROR
+		}
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('_exception_handler'))
+{
+	/**
+	 * Exception Handler
+	 *
+	 * Sends uncaught exceptions to the logger and displays them
+	 * only if display_errors is On so that they don't show up in
+	 * production environments.
+	 *
+	 * @param	Exception	$exception
+	 * @return	void
+	 */
+	function _exception_handler($exception)
+	{
+		$_error =& load_class('Exceptions', 'core');
+		$_error->log_exception('error', 'Exception: '.$exception->getMessage(), $exception->getFile(), $exception->getLine());
+
+		is_cli() OR set_status_header(500);
+		// Should we display the error?
+		if (str_ireplace(array('off', 'none', 'no', 'false', 'null'), '', ini_get('display_errors')))
+		{
+			$_error->show_exception($exception);
+		}
+
+		exit(1); // EXIT_ERROR
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('_shutdown_handler'))
+{
+	/**
+	 * Shutdown Handler
+	 *
+	 * This is the shutdown handler that is declared at the top
+	 * of CodeIgniter.php. The main reason we use this is to simulate
+	 * a complete custom exception handler.
+	 *
+	 * E_STRICT is purposively neglected because such events may have
+	 * been caught. Duplication or none? None is preferred for now.
+	 *
+	 * @link	http://insomanic.me.uk/post/229851073/php-trick-catching-fatal-errors-e-error-with-a
+	 * @return	void
+	 */
+	function _shutdown_handler()
+	{
+		$last_error = error_get_last();
+		if (isset($last_error) &&
+			($last_error['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR | E_CORE_WARNING | E_COMPILE_ERROR | E_COMPILE_WARNING)))
+		{
+			_error_handler($last_error['type'], $last_error['message'], $last_error['file'], $last_error['line']);
+		}
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('remove_invisible_characters'))
+{
+	/**
+	 * Remove Invisible Characters
+	 *
+	 * This prevents sandwiching null characters
+	 * between ascii characters, like Java\0script.
+	 *
+	 * @param	string
+	 * @param	bool
+	 * @return	string
+	 */
+	function remove_invisible_characters($str, $url_encoded = TRUE)
+	{
+		$non_displayables = array();
+
+		// every control character except newline (dec 10),
+		// carriage return (dec 13) and horizontal tab (dec 09)
+		if ($url_encoded)
+		{
+			$non_displayables[] = '/%0[0-8bcef]/i';	// url encoded 00-08, 11, 12, 14, 15
+			$non_displayables[] = '/%1[0-9a-f]/i';	// url encoded 16-31
+			$non_displayables[] = '/%7f/i';	// url encoded 127
+		}
+
+		$non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S';	// 00-08, 11, 12, 14-31, 127
+
+		do
+		{
+			$str = preg_replace($non_displayables, '', $str, -1, $count);
+		}
+		while ($count);
+
+		return $str;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('html_escape'))
+{
+	/**
+	 * Returns HTML escaped variable.
+	 *
+	 * @param	mixed	$var		The input string or array of strings to be escaped.
+	 * @param	bool	$double_encode	$double_encode set to FALSE prevents escaping twice.
+	 * @return	mixed			The escaped string or array of strings as a result.
+	 */
+	function html_escape($var, $double_encode = TRUE)
+	{
+		if (empty($var))
+		{
+			return $var;
+		}
+
+		if (is_array($var))
+		{
+			foreach (array_keys($var) as $key)
+			{
+				$var[$key] = html_escape($var[$key], $double_encode);
+			}
+
+			return $var;
+		}
+
+		return htmlspecialchars($var, ENT_QUOTES, config_item('charset'), $double_encode);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('_stringify_attributes'))
+{
+	/**
+	 * Stringify attributes for use in HTML tags.
+	 *
+	 * Helper function used to convert a string, array, or object
+	 * of attributes to a string.
+	 *
+	 * @param	mixed	string, array, object
+	 * @param	bool
+	 * @return	string
+	 */
+	function _stringify_attributes($attributes, $js = FALSE)
+	{
+		if (empty($attributes))
+		{
+			return NULL;
+		}
+
+		if (is_string($attributes))
+		{
+			return ' '.$attributes;
+		}
+
+		$attributes = (array) $attributes;
+
+		$atts = '';
+		foreach ($attributes as $key => $val)
+		{
+			$atts .= ($js) ? $key.'='.$val.',' : ' '.$key.'="'.$val.'"';
+		}
+
+		return rtrim($atts, ',');
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('function_usable'))
+{
+	/**
+	 * Function usable
+	 *
+	 * Executes a function_exists() check, and if the Suhosin PHP
+	 * extension is loaded - checks whether the function that is
+	 * checked might be disabled in there as well.
+	 *
+	 * This is useful as function_exists() will return FALSE for
+	 * functions disabled via the *disable_functions* php.ini
+	 * setting, but not for *suhosin.executor.func.blacklist* and
+	 * *suhosin.executor.disable_eval*. These settings will just
+	 * terminate script execution if a disabled function is executed.
+	 *
+	 * The above described behavior turned out to be a bug in Suhosin,
+	 * but even though a fix was committed for 0.9.34 on 2012-02-12,
+	 * that version is yet to be released. This function will therefore
+	 * be just temporary, but would probably be kept for a few years.
+	 *
+	 * @link	http://www.hardened-php.net/suhosin/
+	 * @param	string	$function_name	Function to check for
+	 * @return	bool	TRUE if the function exists and is safe to call,
+	 *			FALSE otherwise.
+	 */
+	function function_usable($function_name)
+	{
+		static $_suhosin_func_blacklist;
+
+		if (function_exists($function_name))
+		{
+			if ( ! isset($_suhosin_func_blacklist))
+			{
+				$_suhosin_func_blacklist = extension_loaded('suhosin')
+					? explode(',', trim(ini_get('suhosin.executor.func.blacklist')))
+					: array();
+			}
+
+			return ! in_array($function_name, $_suhosin_func_blacklist, TRUE);
+		}
+
+		return FALSE;
+	}
+}
diff --git a/system/core/Config.php b/system/core/Config.php
new file mode 100644
index 0000000..2454a9d
--- /dev/null
+++ b/system/core/Config.php
@@ -0,0 +1,380 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Config Class
+ *
+ * This class contains functions that enable config files to be managed
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/libraries/config.html
+ */
+class CI_Config {
+
+	/**
+	 * List of all loaded config values
+	 *
+	 * @var	array
+	 */
+	public $config = array();
+
+	/**
+	 * List of all loaded config files
+	 *
+	 * @var	array
+	 */
+	public $is_loaded =	array();
+
+	/**
+	 * List of paths to search when trying to load a config file.
+	 *
+	 * @used-by	CI_Loader
+	 * @var		array
+	 */
+	public $_config_paths =	array(APPPATH);
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Sets the $config data from the primary config.php file as a class variable.
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		$this->config =& get_config();
+
+		// Set the base_url automatically if none was provided
+		if (empty($this->config['base_url']))
+		{
+			if (isset($_SERVER['SERVER_ADDR']))
+			{
+				if (strpos($_SERVER['SERVER_ADDR'], ':') !== FALSE)
+				{
+					$server_addr = '['.$_SERVER['SERVER_ADDR'].']';
+				}
+				else
+				{
+					$server_addr = $_SERVER['SERVER_ADDR'];
+				}
+
+				$base_url = (is_https() ? 'https' : 'http').'://'.$server_addr
+					.substr($_SERVER['SCRIPT_NAME'], 0, strpos($_SERVER['SCRIPT_NAME'], basename($_SERVER['SCRIPT_FILENAME'])));
+			}
+			else
+			{
+				$base_url = 'http://localhost/';
+			}
+
+			$this->set_item('base_url', $base_url);
+		}
+
+		log_message('info', 'Config Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Load Config File
+	 *
+	 * @param	string	$file			Configuration file name
+	 * @param	bool	$use_sections		Whether configuration values should be loaded into their own section
+	 * @param	bool	$fail_gracefully	Whether to just return FALSE or display an error message
+	 * @return	bool	TRUE if the file was loaded correctly or FALSE on failure
+	 */
+	public function load($file = '', $use_sections = FALSE, $fail_gracefully = FALSE)
+	{
+		$file = ($file === '') ? 'config' : str_replace('.php', '', $file);
+		$loaded = FALSE;
+
+		foreach ($this->_config_paths as $path)
+		{
+			foreach (array($file, ENVIRONMENT.DIRECTORY_SEPARATOR.$file) as $location)
+			{
+				$file_path = $path.'config/'.$location.'.php';
+				if (in_array($file_path, $this->is_loaded, TRUE))
+				{
+					return TRUE;
+				}
+
+				if ( ! file_exists($file_path))
+				{
+					continue;
+				}
+
+				include($file_path);
+
+				if ( ! isset($config) OR ! is_array($config))
+				{
+					if ($fail_gracefully === TRUE)
+					{
+						return FALSE;
+					}
+
+					show_error('Your '.$file_path.' file does not appear to contain a valid configuration array.');
+				}
+
+				if ($use_sections === TRUE)
+				{
+					$this->config[$file] = isset($this->config[$file])
+						? array_merge($this->config[$file], $config)
+						: $config;
+				}
+				else
+				{
+					$this->config = array_merge($this->config, $config);
+				}
+
+				$this->is_loaded[] = $file_path;
+				$config = NULL;
+				$loaded = TRUE;
+				log_message('debug', 'Config file loaded: '.$file_path);
+			}
+		}
+
+		if ($loaded === TRUE)
+		{
+			return TRUE;
+		}
+		elseif ($fail_gracefully === TRUE)
+		{
+			return FALSE;
+		}
+
+		show_error('The configuration file '.$file.'.php does not exist.');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch a config file item
+	 *
+	 * @param	string	$item	Config item name
+	 * @param	string	$index	Index name
+	 * @return	string|null	The configuration item or NULL if the item doesn't exist
+	 */
+	public function item($item, $index = '')
+	{
+		if ($index == '')
+		{
+			return isset($this->config[$item]) ? $this->config[$item] : NULL;
+		}
+
+		return isset($this->config[$index], $this->config[$index][$item]) ? $this->config[$index][$item] : NULL;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch a config file item with slash appended (if not empty)
+	 *
+	 * @param	string		$item	Config item name
+	 * @return	string|null	The configuration item or NULL if the item doesn't exist
+	 */
+	public function slash_item($item)
+	{
+		if ( ! isset($this->config[$item]))
+		{
+			return NULL;
+		}
+		elseif (trim($this->config[$item]) === '')
+		{
+			return '';
+		}
+
+		return rtrim($this->config[$item], '/').'/';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Site URL
+	 *
+	 * Returns base_url . index_page [. uri_string]
+	 *
+	 * @uses	CI_Config::_uri_string()
+	 *
+	 * @param	string|string[]	$uri	URI string or an array of segments
+	 * @param	string	$protocol
+	 * @return	string
+	 */
+	public function site_url($uri = '', $protocol = NULL)
+	{
+		$base_url = $this->slash_item('base_url');
+
+		if (isset($protocol))
+		{
+			// For protocol-relative links
+			if ($protocol === '')
+			{
+				$base_url = substr($base_url, strpos($base_url, '//'));
+			}
+			else
+			{
+				$base_url = $protocol.substr($base_url, strpos($base_url, '://'));
+			}
+		}
+
+		if (empty($uri))
+		{
+			return $base_url.$this->item('index_page');
+		}
+
+		$uri = $this->_uri_string($uri);
+
+		if ($this->item('enable_query_strings') === FALSE)
+		{
+			$suffix = isset($this->config['url_suffix']) ? $this->config['url_suffix'] : '';
+
+			if ($suffix !== '')
+			{
+				if (($offset = strpos($uri, '?')) !== FALSE)
+				{
+					$uri = substr($uri, 0, $offset).$suffix.substr($uri, $offset);
+				}
+				else
+				{
+					$uri .= $suffix;
+				}
+			}
+
+			return $base_url.$this->slash_item('index_page').$uri;
+		}
+		elseif (strpos($uri, '?') === FALSE)
+		{
+			$uri = '?'.$uri;
+		}
+
+		return $base_url.$this->item('index_page').$uri;
+	}
+
+	// -------------------------------------------------------------
+
+	/**
+	 * Base URL
+	 *
+	 * Returns base_url [. uri_string]
+	 *
+	 * @uses	CI_Config::_uri_string()
+	 *
+	 * @param	string|string[]	$uri	URI string or an array of segments
+	 * @param	string	$protocol
+	 * @return	string
+	 */
+	public function base_url($uri = '', $protocol = NULL)
+	{
+		$base_url = $this->slash_item('base_url');
+
+		if (isset($protocol))
+		{
+			// For protocol-relative links
+			if ($protocol === '')
+			{
+				$base_url = substr($base_url, strpos($base_url, '//'));
+			}
+			else
+			{
+				$base_url = $protocol.substr($base_url, strpos($base_url, '://'));
+			}
+		}
+
+		return $base_url.$this->_uri_string($uri);
+	}
+
+	// -------------------------------------------------------------
+
+	/**
+	 * Build URI string
+	 *
+	 * @used-by	CI_Config::site_url()
+	 * @used-by	CI_Config::base_url()
+	 *
+	 * @param	string|string[]	$uri	URI string or an array of segments
+	 * @return	string
+	 */
+	protected function _uri_string($uri)
+	{
+		if ($this->item('enable_query_strings') === FALSE)
+		{
+			is_array($uri) && $uri = implode('/', $uri);
+			return ltrim($uri, '/');
+		}
+		elseif (is_array($uri))
+		{
+			return http_build_query($uri);
+		}
+
+		return $uri;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * System URL
+	 *
+	 * @deprecated	3.0.0	Encourages insecure practices
+	 * @return	string
+	 */
+	public function system_url()
+	{
+		$x = explode('/', preg_replace('|/*(.+?)/*$|', '\\1', BASEPATH));
+		return $this->slash_item('base_url').end($x).'/';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set a config file item
+	 *
+	 * @param	string	$item	Config item key
+	 * @param	string	$value	Config item value
+	 * @return	void
+	 */
+	public function set_item($item, $value)
+	{
+		$this->config[$item] = $value;
+	}
+
+}
diff --git a/system/core/Controller.php b/system/core/Controller.php
new file mode 100644
index 0000000..aeccd60
--- /dev/null
+++ b/system/core/Controller.php
@@ -0,0 +1,104 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Application Controller Class
+ *
+ * This class object is the super class that every library in
+ * CodeIgniter will be assigned to.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/general/controllers.html
+ */
+class CI_Controller {
+
+	/**
+	 * Reference to the CI singleton
+	 *
+	 * @var	object
+	 */
+	private static $instance;
+
+	/**
+	 * CI_Loader
+	 *
+	 * @var	CI_Loader
+	 */
+	public $load;
+
+	/**
+	 * Class constructor
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		self::$instance =& $this;
+
+		// Assign all the class objects that were instantiated by the
+		// bootstrap file (CodeIgniter.php) to local class variables
+		// so that CI can run as one big super object.
+		foreach (is_loaded() as $var => $class)
+		{
+			$this->$var =& load_class($class);
+		}
+
+		$this->load =& load_class('Loader', 'core');
+		$this->load->initialize();
+		log_message('info', 'Controller Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get the CI singleton
+	 *
+	 * @static
+	 * @return	object
+	 */
+	public static function &get_instance()
+	{
+		return self::$instance;
+	}
+
+}
diff --git a/system/core/Exceptions.php b/system/core/Exceptions.php
new file mode 100644
index 0000000..b1bc2de
--- /dev/null
+++ b/system/core/Exceptions.php
@@ -0,0 +1,275 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Exceptions Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Exceptions
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/libraries/exceptions.html
+ */
+class CI_Exceptions {
+
+	/**
+	 * Nesting level of the output buffering mechanism
+	 *
+	 * @var	int
+	 */
+	public $ob_level;
+
+	/**
+	 * List of available error levels
+	 *
+	 * @var	array
+	 */
+	public $levels = array(
+		E_ERROR			=>	'Error',
+		E_WARNING		=>	'Warning',
+		E_PARSE			=>	'Parsing Error',
+		E_NOTICE		=>	'Notice',
+		E_CORE_ERROR		=>	'Core Error',
+		E_CORE_WARNING		=>	'Core Warning',
+		E_COMPILE_ERROR		=>	'Compile Error',
+		E_COMPILE_WARNING	=>	'Compile Warning',
+		E_USER_ERROR		=>	'User Error',
+		E_USER_WARNING		=>	'User Warning',
+		E_USER_NOTICE		=>	'User Notice',
+		E_STRICT		=>	'Runtime Notice'
+	);
+
+	/**
+	 * Class constructor
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		$this->ob_level = ob_get_level();
+		// Note: Do not log messages from this constructor.
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Exception Logger
+	 *
+	 * Logs PHP generated error messages
+	 *
+	 * @param	int	$severity	Log level
+	 * @param	string	$message	Error message
+	 * @param	string	$filepath	File path
+	 * @param	int	$line		Line number
+	 * @return	void
+	 */
+	public function log_exception($severity, $message, $filepath, $line)
+	{
+		$severity = isset($this->levels[$severity]) ? $this->levels[$severity] : $severity;
+		log_message('error', 'Severity: '.$severity.' --> '.$message.' '.$filepath.' '.$line);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * 404 Error Handler
+	 *
+	 * @uses	CI_Exceptions::show_error()
+	 *
+	 * @param	string	$page		Page URI
+	 * @param 	bool	$log_error	Whether to log the error
+	 * @return	void
+	 */
+	public function show_404($page = '', $log_error = TRUE)
+	{
+		if (is_cli())
+		{
+			$heading = 'Not Found';
+			$message = 'The controller/method pair you requested was not found.';
+		}
+		else
+		{
+			$heading = '404 Page Not Found';
+			$message = 'The page you requested was not found.';
+		}
+
+		// By default we log this, but allow a dev to skip it
+		if ($log_error)
+		{
+			log_message('error', $heading.': '.$page);
+		}
+
+		echo $this->show_error($heading, $message, 'error_404', 404);
+		exit(4); // EXIT_UNKNOWN_FILE
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * General Error Page
+	 *
+	 * Takes an error message as input (either as a string or an array)
+	 * and displays it using the specified template.
+	 *
+	 * @param	string		$heading	Page heading
+	 * @param	string|string[]	$message	Error message
+	 * @param	string		$template	Template name
+	 * @param 	int		$status_code	(default: 500)
+	 *
+	 * @return	string	Error page output
+	 */
+	public function show_error($heading, $message, $template = 'error_general', $status_code = 500)
+	{
+		$templates_path = config_item('error_views_path');
+		if (empty($templates_path))
+		{
+			$templates_path = VIEWPATH.'errors'.DIRECTORY_SEPARATOR;
+		}
+
+		if (is_cli())
+		{
+			$message = "\t".(is_array($message) ? implode("\n\t", $message) : $message);
+			$template = 'cli'.DIRECTORY_SEPARATOR.$template;
+		}
+		else
+		{
+			set_status_header($status_code);
+			$message = '<p>'.(is_array($message) ? implode('</p><p>', $message) : $message).'</p>';
+			$template = 'html'.DIRECTORY_SEPARATOR.$template;
+		}
+
+		if (ob_get_level() > $this->ob_level + 1)
+		{
+			ob_end_flush();
+		}
+		ob_start();
+		include($templates_path.$template.'.php');
+		$buffer = ob_get_contents();
+		ob_end_clean();
+		return $buffer;
+	}
+
+	// --------------------------------------------------------------------
+
+	public function show_exception($exception)
+	{
+		$templates_path = config_item('error_views_path');
+		if (empty($templates_path))
+		{
+			$templates_path = VIEWPATH.'errors'.DIRECTORY_SEPARATOR;
+		}
+
+		$message = $exception->getMessage();
+		if (empty($message))
+		{
+			$message = '(null)';
+		}
+
+		if (is_cli())
+		{
+			$templates_path .= 'cli'.DIRECTORY_SEPARATOR;
+		}
+		else
+		{
+			$templates_path .= 'html'.DIRECTORY_SEPARATOR;
+		}
+
+		if (ob_get_level() > $this->ob_level + 1)
+		{
+			ob_end_flush();
+		}
+
+		ob_start();
+		include($templates_path.'error_exception.php');
+		$buffer = ob_get_contents();
+		ob_end_clean();
+		echo $buffer;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Native PHP error handler
+	 *
+	 * @param	int	$severity	Error level
+	 * @param	string	$message	Error message
+	 * @param	string	$filepath	File path
+	 * @param	int	$line		Line number
+	 * @return	void
+	 */
+	public function show_php_error($severity, $message, $filepath, $line)
+	{
+		$templates_path = config_item('error_views_path');
+		if (empty($templates_path))
+		{
+			$templates_path = VIEWPATH.'errors'.DIRECTORY_SEPARATOR;
+		}
+
+		$severity = isset($this->levels[$severity]) ? $this->levels[$severity] : $severity;
+
+		// For safety reasons we don't show the full file path in non-CLI requests
+		if ( ! is_cli())
+		{
+			$filepath = str_replace('\\', '/', $filepath);
+			if (FALSE !== strpos($filepath, '/'))
+			{
+				$x = explode('/', $filepath);
+				$filepath = $x[count($x)-2].'/'.end($x);
+			}
+
+			$template = 'html'.DIRECTORY_SEPARATOR.'error_php';
+		}
+		else
+		{
+			$template = 'cli'.DIRECTORY_SEPARATOR.'error_php';
+		}
+
+		if (ob_get_level() > $this->ob_level + 1)
+		{
+			ob_end_flush();
+		}
+		ob_start();
+		include($templates_path.$template.'.php');
+		$buffer = ob_get_contents();
+		ob_end_clean();
+		echo $buffer;
+	}
+
+}
diff --git a/system/core/Hooks.php b/system/core/Hooks.php
new file mode 100644
index 0000000..2246bbc
--- /dev/null
+++ b/system/core/Hooks.php
@@ -0,0 +1,267 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Hooks Class
+ *
+ * Provides a mechanism to extend the base system without hacking.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/general/hooks.html
+ */
+class CI_Hooks {
+
+	/**
+	 * Determines whether hooks are enabled
+	 *
+	 * @var	bool
+	 */
+	public $enabled = FALSE;
+
+	/**
+	 * List of all hooks set in config/hooks.php
+	 *
+	 * @var	array
+	 */
+	public $hooks =	array();
+
+	/**
+	 * Array with class objects to use hooks methods
+	 *
+	 * @var array
+	 */
+	protected $_objects = array();
+
+	/**
+	 * In progress flag
+	 *
+	 * Determines whether hook is in progress, used to prevent infinte loops
+	 *
+	 * @var	bool
+	 */
+	protected $_in_progress = FALSE;
+
+	/**
+	 * Class constructor
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		$CFG =& load_class('Config', 'core');
+		log_message('info', 'Hooks Class Initialized');
+
+		// If hooks are not enabled in the config file
+		// there is nothing else to do
+		if ($CFG->item('enable_hooks') === FALSE)
+		{
+			return;
+		}
+
+		// Grab the "hooks" definition file.
+		if (file_exists(APPPATH.'config/hooks.php'))
+		{
+			include(APPPATH.'config/hooks.php');
+		}
+
+		if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/hooks.php'))
+		{
+			include(APPPATH.'config/'.ENVIRONMENT.'/hooks.php');
+		}
+
+		// If there are no hooks, we're done.
+		if ( ! isset($hook) OR ! is_array($hook))
+		{
+			return;
+		}
+
+		$this->hooks =& $hook;
+		$this->enabled = TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Call Hook
+	 *
+	 * Calls a particular hook. Called by CodeIgniter.php.
+	 *
+	 * @uses	CI_Hooks::_run_hook()
+	 *
+	 * @param	string	$which	Hook name
+	 * @return	bool	TRUE on success or FALSE on failure
+	 */
+	public function call_hook($which = '')
+	{
+		if ( ! $this->enabled OR ! isset($this->hooks[$which]))
+		{
+			return FALSE;
+		}
+
+		if (is_array($this->hooks[$which]) && ! isset($this->hooks[$which]['function']))
+		{
+			foreach ($this->hooks[$which] as $val)
+			{
+				$this->_run_hook($val);
+			}
+		}
+		else
+		{
+			$this->_run_hook($this->hooks[$which]);
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Run Hook
+	 *
+	 * Runs a particular hook
+	 *
+	 * @param	array	$data	Hook details
+	 * @return	bool	TRUE on success or FALSE on failure
+	 */
+	protected function _run_hook($data)
+	{
+		// Closures/lambda functions and array($object, 'method') callables
+		if (is_callable($data))
+		{
+			is_array($data)
+				? $data[0]->{$data[1]}()
+				: $data();
+
+			return TRUE;
+		}
+		elseif ( ! is_array($data))
+		{
+			return FALSE;
+		}
+
+		// -----------------------------------
+		// Safety - Prevents run-away loops
+		// -----------------------------------
+
+		// If the script being called happens to have the same
+		// hook call within it a loop can happen
+		if ($this->_in_progress === TRUE)
+		{
+			return;
+		}
+
+		// -----------------------------------
+		// Set file path
+		// -----------------------------------
+
+		if ( ! isset($data['filepath'], $data['filename']))
+		{
+			return FALSE;
+		}
+
+		$filepath = APPPATH.$data['filepath'].'/'.$data['filename'];
+
+		if ( ! file_exists($filepath))
+		{
+			return FALSE;
+		}
+
+		// Determine and class and/or function names
+		$class		= empty($data['class']) ? FALSE : $data['class'];
+		$function	= empty($data['function']) ? FALSE : $data['function'];
+		$params		= isset($data['params']) ? $data['params'] : '';
+
+		if (empty($function))
+		{
+			return FALSE;
+		}
+
+		// Set the _in_progress flag
+		$this->_in_progress = TRUE;
+
+		// Call the requested class and/or function
+		if ($class !== FALSE)
+		{
+			// The object is stored?
+			if (isset($this->_objects[$class]))
+			{
+				if (method_exists($this->_objects[$class], $function))
+				{
+					$this->_objects[$class]->$function($params);
+				}
+				else
+				{
+					return $this->_in_progress = FALSE;
+				}
+			}
+			else
+			{
+				class_exists($class, FALSE) OR require_once($filepath);
+
+				if ( ! class_exists($class, FALSE) OR ! method_exists($class, $function))
+				{
+					return $this->_in_progress = FALSE;
+				}
+
+				// Store the object and execute the method
+				$this->_objects[$class] = new $class();
+				$this->_objects[$class]->$function($params);
+			}
+		}
+		else
+		{
+			function_exists($function) OR require_once($filepath);
+
+			if ( ! function_exists($function))
+			{
+				return $this->_in_progress = FALSE;
+			}
+
+			$function($params);
+		}
+
+		$this->_in_progress = FALSE;
+		return TRUE;
+	}
+
+}
diff --git a/system/core/Input.php b/system/core/Input.php
new file mode 100644
index 0000000..eba5f67
--- /dev/null
+++ b/system/core/Input.php
@@ -0,0 +1,937 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Input Class
+ *
+ * Pre-processes global input data for security
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Input
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/libraries/input.html
+ */
+class CI_Input {
+
+	/**
+	 * IP address of the current user
+	 *
+	 * @var	string
+	 */
+	protected $ip_address = FALSE;
+
+	/**
+	 * Allow GET array flag
+	 *
+	 * If set to FALSE, then $_GET will be set to an empty array.
+	 *
+	 * @var	bool
+	 */
+	protected $_allow_get_array = TRUE;
+
+	/**
+	 * Standardize new lines flag
+	 *
+	 * If set to TRUE, then newlines are standardized.
+	 *
+	 * @var	bool
+	 */
+	protected $_standardize_newlines;
+
+	/**
+	 * Enable XSS flag
+	 *
+	 * Determines whether the XSS filter is always active when
+	 * GET, POST or COOKIE data is encountered.
+	 * Set automatically based on config setting.
+	 *
+	 * @var	bool
+	 */
+	protected $_enable_xss = FALSE;
+
+	/**
+	 * Enable CSRF flag
+	 *
+	 * Enables a CSRF cookie token to be set.
+	 * Set automatically based on config setting.
+	 *
+	 * @var	bool
+	 */
+	protected $_enable_csrf = FALSE;
+
+	/**
+	 * List of all HTTP request headers
+	 *
+	 * @var array
+	 */
+	protected $headers = array();
+
+	/**
+	 * Raw input stream data
+	 *
+	 * Holds a cache of php://input contents
+	 *
+	 * @var	string
+	 */
+	protected $_raw_input_stream;
+
+	/**
+	 * Parsed input stream data
+	 *
+	 * Parsed from php://input at runtime
+	 *
+	 * @see	CI_Input::input_stream()
+	 * @var	array
+	 */
+	protected $_input_stream;
+
+	protected $security;
+	protected $uni;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Determines whether to globally enable the XSS processing
+	 * and whether to allow the $_GET array.
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		$this->_allow_get_array		= (config_item('allow_get_array') !== FALSE);
+		$this->_enable_xss		= (config_item('global_xss_filtering') === TRUE);
+		$this->_enable_csrf		= (config_item('csrf_protection') === TRUE);
+		$this->_standardize_newlines	= (bool) config_item('standardize_newlines');
+
+		$this->security =& load_class('Security', 'core');
+
+		// Do we need the UTF-8 class?
+		if (UTF8_ENABLED === TRUE)
+		{
+			$this->uni =& load_class('Utf8', 'core');
+		}
+
+		// Sanitize global arrays
+		$this->_sanitize_globals();
+
+		// CSRF Protection check
+		if ($this->_enable_csrf === TRUE && ! is_cli())
+		{
+			$this->security->csrf_verify();
+		}
+
+		log_message('info', 'Input Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch from array
+	 *
+	 * Internal method used to retrieve values from global arrays.
+	 *
+	 * @param	array	&$array		$_GET, $_POST, $_COOKIE, $_SERVER, etc.
+	 * @param	mixed	$index		Index for item to be fetched from $array
+	 * @param	bool	$xss_clean	Whether to apply XSS filtering
+	 * @return	mixed
+	 */
+	protected function _fetch_from_array(&$array, $index = NULL, $xss_clean = NULL)
+	{
+		is_bool($xss_clean) OR $xss_clean = $this->_enable_xss;
+
+		// If $index is NULL, it means that the whole $array is requested
+		isset($index) OR $index = array_keys($array);
+
+		// allow fetching multiple keys at once
+		if (is_array($index))
+		{
+			$output = array();
+			foreach ($index as $key)
+			{
+				$output[$key] = $this->_fetch_from_array($array, $key, $xss_clean);
+			}
+
+			return $output;
+		}
+
+		if (isset($array[$index]))
+		{
+			$value = $array[$index];
+		}
+		elseif (($count = preg_match_all('/(?:^[^\[]+)|\[[^]]*\]/', $index, $matches)) > 1) // Does the index contain array notation
+		{
+			$value = $array;
+			for ($i = 0; $i < $count; $i++)
+			{
+				$key = trim($matches[0][$i], '[]');
+				if ($key === '') // Empty notation will return the value as array
+				{
+					break;
+				}
+
+				if (isset($value[$key]))
+				{
+					$value = $value[$key];
+				}
+				else
+				{
+					return NULL;
+				}
+			}
+		}
+		else
+		{
+			return NULL;
+		}
+
+		return ($xss_clean === TRUE)
+			? $this->security->xss_clean($value)
+			: $value;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch an item from the GET array
+	 *
+	 * @param	mixed	$index		Index for item to be fetched from $_GET
+	 * @param	bool	$xss_clean	Whether to apply XSS filtering
+	 * @return	mixed
+	 */
+	public function get($index = NULL, $xss_clean = NULL)
+	{
+		return $this->_fetch_from_array($_GET, $index, $xss_clean);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch an item from the POST array
+	 *
+	 * @param	mixed	$index		Index for item to be fetched from $_POST
+	 * @param	bool	$xss_clean	Whether to apply XSS filtering
+	 * @return	mixed
+	 */
+	public function post($index = NULL, $xss_clean = NULL)
+	{
+		return $this->_fetch_from_array($_POST, $index, $xss_clean);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch an item from POST data with fallback to GET
+	 *
+	 * @param	string	$index		Index for item to be fetched from $_POST or $_GET
+	 * @param	bool	$xss_clean	Whether to apply XSS filtering
+	 * @return	mixed
+	 */
+	public function post_get($index, $xss_clean = NULL)
+	{
+		return isset($_POST[$index])
+			? $this->post($index, $xss_clean)
+			: $this->get($index, $xss_clean);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch an item from GET data with fallback to POST
+	 *
+	 * @param	string	$index		Index for item to be fetched from $_GET or $_POST
+	 * @param	bool	$xss_clean	Whether to apply XSS filtering
+	 * @return	mixed
+	 */
+	public function get_post($index, $xss_clean = NULL)
+	{
+		return isset($_GET[$index])
+			? $this->get($index, $xss_clean)
+			: $this->post($index, $xss_clean);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch an item from the COOKIE array
+	 *
+	 * @param	mixed	$index		Index for item to be fetched from $_COOKIE
+	 * @param	bool	$xss_clean	Whether to apply XSS filtering
+	 * @return	mixed
+	 */
+	public function cookie($index = NULL, $xss_clean = NULL)
+	{
+		return $this->_fetch_from_array($_COOKIE, $index, $xss_clean);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch an item from the SERVER array
+	 *
+	 * @param	mixed	$index		Index for item to be fetched from $_SERVER
+	 * @param	bool	$xss_clean	Whether to apply XSS filtering
+	 * @return	mixed
+	 */
+	public function server($index, $xss_clean = NULL)
+	{
+		return $this->_fetch_from_array($_SERVER, $index, $xss_clean);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Fetch an item from the php://input stream
+	 *
+	 * Useful when you need to access PUT, DELETE or PATCH request data.
+	 *
+	 * @param	string	$index		Index for item to be fetched
+	 * @param	bool	$xss_clean	Whether to apply XSS filtering
+	 * @return	mixed
+	 */
+	public function input_stream($index = NULL, $xss_clean = NULL)
+	{
+		// Prior to PHP 5.6, the input stream can only be read once,
+		// so we'll need to check if we have already done that first.
+		if ( ! is_array($this->_input_stream))
+		{
+			// $this->raw_input_stream will trigger __get().
+			parse_str($this->raw_input_stream, $this->_input_stream);
+			is_array($this->_input_stream) OR $this->_input_stream = array();
+		}
+
+		return $this->_fetch_from_array($this->_input_stream, $index, $xss_clean);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Set cookie
+	 *
+	 * Accepts an arbitrary number of parameters (up to 7) or an associative
+	 * array in the first parameter containing all the values.
+	 *
+	 * @param	string|mixed[]	$name		Cookie name or an array containing parameters
+	 * @param	string		$value		Cookie value
+	 * @param	int		$expire		Cookie expiration time in seconds
+	 * @param	string		$domain		Cookie domain (e.g.: '.yourdomain.com')
+	 * @param	string		$path		Cookie path (default: '/')
+	 * @param	string		$prefix		Cookie name prefix
+	 * @param	bool		$secure		Whether to only transfer cookies via SSL
+	 * @param	bool		$httponly	Whether to only makes the cookie accessible via HTTP (no javascript)
+	 * @param	string		$samesite	SameSite attribute
+	 * @return	void
+	 */
+	public function set_cookie($name, $value = '', $expire = '', $domain = '', $path = '/', $prefix = '', $secure = NULL, $httponly = NULL, $samesite = NULL)
+	{
+		if (is_array($name))
+		{
+			// always leave 'name' in last place, as the loop will break otherwise, due to $$item
+			foreach (array('value', 'expire', 'domain', 'path', 'prefix', 'secure', 'httponly', 'name', 'samesite') as $item)
+			{
+				if (isset($name[$item]))
+				{
+					$$item = $name[$item];
+				}
+			}
+		}
+
+		if ($prefix === '' && config_item('cookie_prefix') !== '')
+		{
+			$prefix = config_item('cookie_prefix');
+		}
+
+		if ($domain == '' && config_item('cookie_domain') != '')
+		{
+			$domain = config_item('cookie_domain');
+		}
+
+		if ($path === '/' && config_item('cookie_path') !== '/')
+		{
+			$path = config_item('cookie_path');
+		}
+
+		$secure = ($secure === NULL && config_item('cookie_secure') !== NULL)
+			? (bool) config_item('cookie_secure')
+			: (bool) $secure;
+
+		$httponly = ($httponly === NULL && config_item('cookie_httponly') !== NULL)
+			? (bool) config_item('cookie_httponly')
+			: (bool) $httponly;
+
+		if ( ! is_numeric($expire))
+		{
+			$expire = time() - 86500;
+		}
+		else
+		{
+			$expire = ($expire > 0) ? time() + $expire : 0;
+		}
+
+		isset($samesite) OR $samesite = config_item('cookie_samesite');
+		if (isset($samesite))
+		{
+			$samesite = ucfirst(strtolower($samesite));
+			in_array($samesite, array('Lax', 'Strict', 'None'), TRUE) OR $samesite = 'Lax';
+		}
+		else
+		{
+			$samesite = 'Lax';
+		}
+
+		if ($samesite === 'None' && ! $secure)
+		{
+			log_message('error', $name.' cookie sent with SameSite=None, but without Secure attribute.');
+		}
+
+		if ( ! is_php('7.3'))
+		{
+			$maxage = $expire - time();
+			if ($maxage < 1)
+			{
+				$maxage = 0;
+			}
+
+			$cookie_header = 'Set-Cookie: '.$prefix.$name.'='.rawurlencode($value);
+			$cookie_header .= ($expire === 0 ? '' : '; Expires='.gmdate('D, d-M-Y H:i:s T', $expire)).'; Max-Age='.$maxage;
+			$cookie_header .= '; Path='.$path.($domain !== '' ? '; Domain='.$domain : '');
+			$cookie_header .= ($secure ? '; Secure' : '').($httponly ? '; HttpOnly' : '').'; SameSite='.$samesite;
+			header($cookie_header);
+			return;
+		}
+
+		$setcookie_options = array(
+			'expires' => $expire,
+			'path' => $path,
+			'domain' => $domain,
+			'secure' => $secure,
+			'httponly' => $httponly,
+			'samesite' => $samesite,
+		);
+		setcookie($prefix.$name, $value, $setcookie_options);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch the IP Address
+	 *
+	 * Determines and validates the visitor's IP address.
+	 *
+	 * @return	string	IP address
+	 */
+	public function ip_address()
+	{
+		if ($this->ip_address !== FALSE)
+		{
+			return $this->ip_address;
+		}
+
+		$proxy_ips = config_item('proxy_ips');
+		if ( ! empty($proxy_ips) && ! is_array($proxy_ips))
+		{
+			$proxy_ips = explode(',', str_replace(' ', '', $proxy_ips));
+		}
+
+		$this->ip_address = $this->server('REMOTE_ADDR');
+
+		if ($proxy_ips)
+		{
+			foreach (array('HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'HTTP_X_CLIENT_IP', 'HTTP_X_CLUSTER_CLIENT_IP') as $header)
+			{
+				if (($spoof = $this->server($header)) !== NULL)
+				{
+					// Some proxies typically list the whole chain of IP
+					// addresses through which the client has reached us.
+					// e.g. client_ip, proxy_ip1, proxy_ip2, etc.
+					sscanf($spoof, '%[^,]', $spoof);
+
+					if ( ! $this->valid_ip($spoof))
+					{
+						$spoof = NULL;
+					}
+					else
+					{
+						break;
+					}
+				}
+			}
+
+			if ($spoof)
+			{
+				for ($i = 0, $c = count($proxy_ips); $i < $c; $i++)
+				{
+					// Check if we have an IP address or a subnet
+					if (strpos($proxy_ips[$i], '/') === FALSE)
+					{
+						// An IP address (and not a subnet) is specified.
+						// We can compare right away.
+						if ($proxy_ips[$i] === $this->ip_address)
+						{
+							$this->ip_address = $spoof;
+							break;
+						}
+
+						continue;
+					}
+
+					// We have a subnet ... now the heavy lifting begins
+					isset($separator) OR $separator = $this->valid_ip($this->ip_address, 'ipv6') ? ':' : '.';
+
+					// If the proxy entry doesn't match the IP protocol - skip it
+					if (strpos($proxy_ips[$i], $separator) === FALSE)
+					{
+						continue;
+					}
+
+					// Convert the REMOTE_ADDR IP address to binary, if needed
+					if ( ! isset($ip, $sprintf))
+					{
+						if ($separator === ':')
+						{
+							// Make sure we're have the "full" IPv6 format
+							$ip = explode(':',
+								str_replace('::',
+									str_repeat(':', 9 - substr_count($this->ip_address, ':')),
+									$this->ip_address
+								)
+							);
+
+							for ($j = 0; $j < 8; $j++)
+							{
+								$ip[$j] = intval($ip[$j], 16);
+							}
+
+							$sprintf = '%016b%016b%016b%016b%016b%016b%016b%016b';
+						}
+						else
+						{
+							$ip = explode('.', $this->ip_address);
+							$sprintf = '%08b%08b%08b%08b';
+						}
+
+						$ip = vsprintf($sprintf, $ip);
+					}
+
+					// Split the netmask length off the network address
+					sscanf($proxy_ips[$i], '%[^/]/%d', $netaddr, $masklen);
+
+					// Again, an IPv6 address is most likely in a compressed form
+					if ($separator === ':')
+					{
+						$netaddr = explode(':', str_replace('::', str_repeat(':', 9 - substr_count($netaddr, ':')), $netaddr));
+						for ($j = 0; $j < 8; $j++)
+						{
+							$netaddr[$j] = intval($netaddr[$j], 16);
+						}
+					}
+					else
+					{
+						$netaddr = explode('.', $netaddr);
+					}
+
+					// Convert to binary and finally compare
+					if (strncmp($ip, vsprintf($sprintf, $netaddr), $masklen) === 0)
+					{
+						$this->ip_address = $spoof;
+						break;
+					}
+				}
+			}
+		}
+
+		if ( ! $this->valid_ip($this->ip_address))
+		{
+			return $this->ip_address = '0.0.0.0';
+		}
+
+		return $this->ip_address;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate IP Address
+	 *
+	 * @param	string	$ip	IP address
+	 * @param	string	$which	IP protocol: 'ipv4' or 'ipv6'
+	 * @return	bool
+	 */
+	public function valid_ip($ip, $which = '')
+	{
+		switch (strtolower($which))
+		{
+			case 'ipv4':
+				$which = FILTER_FLAG_IPV4;
+				break;
+			case 'ipv6':
+				$which = FILTER_FLAG_IPV6;
+				break;
+			default:
+				$which = 0;
+				break;
+		}
+
+		return (bool) filter_var($ip, FILTER_VALIDATE_IP, $which);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch User Agent string
+	 *
+	 * @return	string|null	User Agent string or NULL if it doesn't exist
+	 */
+	public function user_agent($xss_clean = NULL)
+	{
+		return $this->_fetch_from_array($_SERVER, 'HTTP_USER_AGENT', $xss_clean);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Sanitize Globals
+	 *
+	 * Internal method serving for the following purposes:
+	 *
+	 *	- Unsets $_GET data, if query strings are not enabled
+	 *	- Cleans POST, COOKIE and SERVER data
+	 * 	- Standardizes newline characters to PHP_EOL
+	 *
+	 * @return	void
+	 */
+	protected function _sanitize_globals()
+	{
+		// Is $_GET data allowed? If not we'll set the $_GET to an empty array
+		if ($this->_allow_get_array === FALSE)
+		{
+			$_GET = array();
+		}
+		elseif (is_array($_GET))
+		{
+			foreach ($_GET as $key => $val)
+			{
+				$_GET[$this->_clean_input_keys($key)] = $this->_clean_input_data($val);
+			}
+		}
+
+		// Clean $_POST Data
+		if (is_array($_POST))
+		{
+			foreach ($_POST as $key => $val)
+			{
+				$_POST[$this->_clean_input_keys($key)] = $this->_clean_input_data($val);
+			}
+		}
+
+		// Clean $_COOKIE Data
+		if (is_array($_COOKIE))
+		{
+			// Also get rid of specially treated cookies that might be set by a server
+			// or silly application, that are of no use to a CI application anyway
+			// but that when present will trip our 'Disallowed Key Characters' alarm
+			// http://www.ietf.org/rfc/rfc2109.txt
+			// note that the key names below are single quoted strings, and are not PHP variables
+			unset(
+				$_COOKIE['$Version'],
+				$_COOKIE['$Path'],
+				$_COOKIE['$Domain']
+			);
+
+			foreach ($_COOKIE as $key => $val)
+			{
+				if (($cookie_key = $this->_clean_input_keys($key)) !== FALSE)
+				{
+					$_COOKIE[$cookie_key] = $this->_clean_input_data($val);
+				}
+				else
+				{
+					unset($_COOKIE[$key]);
+				}
+			}
+		}
+
+		// Sanitize PHP_SELF
+		$_SERVER['PHP_SELF'] = strip_tags($_SERVER['PHP_SELF']);
+
+		log_message('debug', 'Global POST, GET and COOKIE data sanitized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Clean Input Data
+	 *
+	 * Internal method that aids in escaping data and
+	 * standardizing newline characters to PHP_EOL.
+	 *
+	 * @param	string|string[]	$str	Input string(s)
+	 * @return	string
+	 */
+	protected function _clean_input_data($str)
+	{
+		if (is_array($str))
+		{
+			$new_array = array();
+			foreach (array_keys($str) as $key)
+			{
+				$new_array[$this->_clean_input_keys($key)] = $this->_clean_input_data($str[$key]);
+			}
+			return $new_array;
+		}
+
+		/* We strip slashes if magic quotes is on to keep things consistent
+
+		   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())
+		{
+			$str = stripslashes($str);
+		}
+
+		// Clean UTF-8 if supported
+		if (UTF8_ENABLED === TRUE)
+		{
+			$str = $this->uni->clean_string($str);
+		}
+
+		// Remove control characters
+		$str = remove_invisible_characters($str, FALSE);
+
+		// Standardize newlines if needed
+		if ($this->_standardize_newlines === TRUE)
+		{
+			return preg_replace('/(?:\r\n|[\r\n])/', PHP_EOL, $str);
+		}
+
+		return $str;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Clean Keys
+	 *
+	 * Internal method that helps to prevent malicious users
+	 * from trying to exploit keys we make sure that keys are
+	 * only named with alpha-numeric text and a few other items.
+	 *
+	 * @param	string	$str	Input string
+	 * @param	bool	$fatal	Whether to terminate script exection
+	 *				or to return FALSE if an invalid
+	 *				key is encountered
+	 * @return	string|bool
+	 */
+	protected function _clean_input_keys($str, $fatal = TRUE)
+	{
+		if ( ! preg_match('/^[a-z0-9:_\/|-]+$/i', $str))
+		{
+			if ($fatal === TRUE)
+			{
+				return FALSE;
+			}
+			else
+			{
+				set_status_header(503);
+				echo 'Disallowed Key Characters.';
+				exit(7); // EXIT_USER_INPUT
+			}
+		}
+
+		// Clean UTF-8 if supported
+		if (UTF8_ENABLED === TRUE)
+		{
+			return $this->uni->clean_string($str);
+		}
+
+		return $str;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Request Headers
+	 *
+	 * @param	bool	$xss_clean	Whether to apply XSS filtering
+	 * @return	array
+	 */
+	public function request_headers($xss_clean = FALSE)
+	{
+		// If header is already defined, return it immediately
+		if ( ! empty($this->headers))
+		{
+			return $this->_fetch_from_array($this->headers, NULL, $xss_clean);
+		}
+
+		// In Apache, you can simply call apache_request_headers()
+		if (function_exists('apache_request_headers'))
+		{
+			$this->headers = apache_request_headers();
+		}
+		else
+		{
+			isset($_SERVER['CONTENT_TYPE']) && $this->headers['Content-Type'] = $_SERVER['CONTENT_TYPE'];
+
+			foreach ($_SERVER as $key => $val)
+			{
+				if (sscanf($key, 'HTTP_%s', $header) === 1)
+				{
+					// take SOME_HEADER and turn it into Some-Header
+					$header = str_replace('_', ' ', strtolower($header));
+					$header = str_replace(' ', '-', ucwords($header));
+
+					$this->headers[$header] = $_SERVER[$key];
+				}
+			}
+		}
+
+		return $this->_fetch_from_array($this->headers, NULL, $xss_clean);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Request Header
+	 *
+	 * Returns the value of a single member of the headers class member
+	 *
+	 * @param	string		$index		Header name
+	 * @param	bool		$xss_clean	Whether to apply XSS filtering
+	 * @return	string|null	The requested header on success or NULL on failure
+	 */
+	public function get_request_header($index, $xss_clean = FALSE)
+	{
+		static $headers;
+
+		if ( ! isset($headers))
+		{
+			empty($this->headers) && $this->request_headers();
+			foreach ($this->headers as $key => $value)
+			{
+				$headers[strtolower($key)] = $value;
+			}
+		}
+
+		$index = strtolower($index);
+
+		if ( ! isset($headers[$index]))
+		{
+			return NULL;
+		}
+
+		return ($xss_clean === TRUE)
+			? $this->security->xss_clean($headers[$index])
+			: $headers[$index];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Is AJAX request?
+	 *
+	 * Test to see if a request contains the HTTP_X_REQUESTED_WITH header.
+	 *
+	 * @return 	bool
+	 */
+	public function is_ajax_request()
+	{
+		return ( ! empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Is CLI request?
+	 *
+	 * Test to see if a request was made from the command line.
+	 *
+	 * @deprecated	3.0.0	Use is_cli() instead
+	 * @return	bool
+	 */
+	public function is_cli_request()
+	{
+		return is_cli();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Request Method
+	 *
+	 * Return the request method
+	 *
+	 * @param	bool	$upper	Whether to return in upper or lower case
+	 *				(default: FALSE)
+	 * @return 	string
+	 */
+	public function method($upper = FALSE)
+	{
+		return ($upper)
+			? strtoupper($this->server('REQUEST_METHOD'))
+			: strtolower($this->server('REQUEST_METHOD'));
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Magic __get()
+	 *
+	 * Allows read access to protected properties
+	 *
+	 * @param	string	$name
+	 * @return	mixed
+	 */
+	public function __get($name)
+	{
+		if ($name === 'raw_input_stream')
+		{
+			isset($this->_raw_input_stream) OR $this->_raw_input_stream = file_get_contents('php://input');
+			return $this->_raw_input_stream;
+		}
+		elseif ($name === 'ip_address')
+		{
+			return $this->ip_address;
+		}
+	}
+
+}
diff --git a/system/core/Lang.php b/system/core/Lang.php
new file mode 100644
index 0000000..1829906
--- /dev/null
+++ b/system/core/Lang.php
@@ -0,0 +1,204 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Language Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Language
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/libraries/language.html
+ */
+class CI_Lang {
+
+	/**
+	 * List of translations
+	 *
+	 * @var	array
+	 */
+	public $language =	array();
+
+	/**
+	 * List of loaded language files
+	 *
+	 * @var	array
+	 */
+	public $is_loaded =	array();
+
+	/**
+	 * Class constructor
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		log_message('info', 'Language Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Load a language file
+	 *
+	 * @param	mixed	$langfile	Language file name
+	 * @param	string	$idiom		Language name (english, etc.)
+	 * @param	bool	$return		Whether to return the loaded array of translations
+	 * @param 	bool	$add_suffix	Whether to add suffix to $langfile
+	 * @param 	string	$alt_path	Alternative path to look for the language file
+	 *
+	 * @return	void|string[]	Array containing translations, if $return is set to TRUE
+	 */
+	public function load($langfile, $idiom = '', $return = FALSE, $add_suffix = TRUE, $alt_path = '')
+	{
+		if (is_array($langfile))
+		{
+			foreach ($langfile as $value)
+			{
+				$this->load($value, $idiom, $return, $add_suffix, $alt_path);
+			}
+
+			return;
+		}
+
+		$langfile = str_replace('.php', '', $langfile);
+
+		if ($add_suffix === TRUE)
+		{
+			$langfile = preg_replace('/_lang$/', '', $langfile).'_lang';
+		}
+
+		$langfile .= '.php';
+
+		if (empty($idiom) OR ! preg_match('/^[a-z_-]+$/i', $idiom))
+		{
+			$config =& get_config();
+			$idiom = empty($config['language']) ? 'english' : $config['language'];
+		}
+
+		if ($return === FALSE && isset($this->is_loaded[$langfile]) && $this->is_loaded[$langfile] === $idiom)
+		{
+			return;
+		}
+
+		// Load the base file, so any others found can override it
+		$basepath = BASEPATH.'language/'.$idiom.'/'.$langfile;
+		if (($found = file_exists($basepath)) === TRUE)
+		{
+			include($basepath);
+		}
+
+		// Do we have an alternative path to look in?
+		if ($alt_path !== '')
+		{
+			$alt_path .= 'language/'.$idiom.'/'.$langfile;
+			if (file_exists($alt_path))
+			{
+				include($alt_path);
+				$found = TRUE;
+			}
+		}
+		else
+		{
+			foreach (get_instance()->load->get_package_paths(TRUE) as $package_path)
+			{
+				$package_path .= 'language/'.$idiom.'/'.$langfile;
+				if ($basepath !== $package_path && file_exists($package_path))
+				{
+					include($package_path);
+					$found = TRUE;
+					break;
+				}
+			}
+		}
+
+		if ($found !== TRUE)
+		{
+			show_error('Unable to load the requested language file: language/'.$idiom.'/'.$langfile);
+		}
+
+		if ( ! isset($lang) OR ! is_array($lang))
+		{
+			log_message('error', 'Language file contains no data: language/'.$idiom.'/'.$langfile);
+
+			if ($return === TRUE)
+			{
+				return array();
+			}
+			return;
+		}
+
+		if ($return === TRUE)
+		{
+			return $lang;
+		}
+
+		$this->is_loaded[$langfile] = $idiom;
+		$this->language = array_merge($this->language, $lang);
+
+		log_message('info', 'Language file loaded: language/'.$idiom.'/'.$langfile);
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Language line
+	 *
+	 * Fetches a single line of text from the language array
+	 *
+	 * @param	string	$line		Language line key
+	 * @param	bool	$log_errors	Whether to log an error message if the line is not found
+	 * @return	string	Translation
+	 */
+	public function line($line, $log_errors = TRUE)
+	{
+		$value = isset($this->language[$line]) ? $this->language[$line] : FALSE;
+
+		// Because killer robots like unicorns!
+		if ($value === FALSE && $log_errors === TRUE)
+		{
+			log_message('error', 'Could not find the language line "'.$line.'"');
+		}
+
+		return $value;
+	}
+
+}
diff --git a/system/core/Loader.php b/system/core/Loader.php
new file mode 100644
index 0000000..a70487e
--- /dev/null
+++ b/system/core/Loader.php
@@ -0,0 +1,1416 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Loader Class
+ *
+ * Loads framework components.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Loader
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/libraries/loader.html
+ */
+class CI_Loader {
+
+	// All these are set automatically. Don't mess with them.
+	/**
+	 * Nesting level of the output buffering mechanism
+	 *
+	 * @var	int
+	 */
+	protected $_ci_ob_level;
+
+	/**
+	 * List of paths to load views from
+	 *
+	 * @var	array
+	 */
+	protected $_ci_view_paths =	array(VIEWPATH	=> TRUE);
+
+	/**
+	 * List of paths to load libraries from
+	 *
+	 * @var	array
+	 */
+	protected $_ci_library_paths =	array(APPPATH, BASEPATH);
+
+	/**
+	 * List of paths to load models from
+	 *
+	 * @var	array
+	 */
+	protected $_ci_model_paths =	array(APPPATH);
+
+	/**
+	 * List of paths to load helpers from
+	 *
+	 * @var	array
+	 */
+	protected $_ci_helper_paths =	array(APPPATH, BASEPATH);
+
+	/**
+	 * List of cached variables
+	 *
+	 * @var	array
+	 */
+	protected $_ci_cached_vars =	array();
+
+	/**
+	 * List of loaded classes
+	 *
+	 * @var	array
+	 */
+	protected $_ci_classes =	array();
+
+	/**
+	 * List of loaded models
+	 *
+	 * @var	array
+	 */
+	protected $_ci_models =	array();
+
+	/**
+	 * List of loaded helpers
+	 *
+	 * @var	array
+	 */
+	protected $_ci_helpers =	array();
+
+	/**
+	 * List of class name mappings
+	 *
+	 * @var	array
+	 */
+	protected $_ci_varmap =	array(
+		'unit_test' => 'unit',
+		'user_agent' => 'agent'
+	);
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Sets component load paths, gets the initial output buffering level.
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		$this->_ci_ob_level = ob_get_level();
+		$this->_ci_classes =& is_loaded();
+
+		log_message('info', 'Loader Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initializer
+	 *
+	 * @todo	Figure out a way to move this to the constructor
+	 *		without breaking *package_path*() methods.
+	 * @uses	CI_Loader::_ci_autoloader()
+	 * @used-by	CI_Controller::__construct()
+	 * @return	void
+	 */
+	public function initialize()
+	{
+		$this->_ci_autoloader();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Is Loaded
+	 *
+	 * A utility method to test if a class is in the self::$_ci_classes array.
+	 *
+	 * @used-by	Mainly used by Form Helper function _get_validation_object().
+	 *
+	 * @param 	string		$class	Class name to check for
+	 * @return 	string|bool	Class object name if loaded or FALSE
+	 */
+	public function is_loaded($class)
+	{
+		return array_search(ucfirst($class), $this->_ci_classes, TRUE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Library Loader
+	 *
+	 * Loads and instantiates libraries.
+	 * Designed to be called from application controllers.
+	 *
+	 * @param	mixed	$library	Library name
+	 * @param	array	$params		Optional parameters to pass to the library class constructor
+	 * @param	string	$object_name	An optional object name to assign to
+	 * @return	object
+	 */
+	public function library($library, $params = NULL, $object_name = NULL)
+	{
+		if (empty($library))
+		{
+			return $this;
+		}
+		elseif (is_array($library))
+		{
+			foreach ($library as $key => $value)
+			{
+				if (is_int($key))
+				{
+					$this->library($value, $params);
+				}
+				else
+				{
+					$this->library($key, $params, $value);
+				}
+			}
+
+			return $this;
+		}
+
+		if ($params !== NULL && ! is_array($params))
+		{
+			$params = NULL;
+		}
+
+		$this->_ci_load_library($library, $params, $object_name);
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Model Loader
+	 *
+	 * Loads and instantiates models.
+	 *
+	 * @param	mixed	$model		Model name
+	 * @param	string	$name		An optional object name to assign to
+	 * @param	bool	$db_conn	An optional database connection configuration to initialize
+	 * @return	object
+	 */
+	public function model($model, $name = '', $db_conn = FALSE)
+	{
+		if (empty($model))
+		{
+			return $this;
+		}
+		elseif (is_array($model))
+		{
+			foreach ($model as $key => $value)
+			{
+				is_int($key) ? $this->model($value, '', $db_conn) : $this->model($key, $value, $db_conn);
+			}
+
+			return $this;
+		}
+
+		$path = '';
+
+		// Is the model in a sub-folder? If so, parse out the filename and path.
+		if (($last_slash = strrpos($model, '/')) !== FALSE)
+		{
+			// The path is in front of the last slash
+			$path = substr($model, 0, ++$last_slash);
+
+			// And the model name behind it
+			$model = substr($model, $last_slash);
+		}
+
+		if (empty($name))
+		{
+			$name = $model;
+		}
+
+		if (in_array($name, $this->_ci_models, TRUE))
+		{
+			return $this;
+		}
+
+		$CI =& get_instance();
+		if (isset($CI->$name))
+		{
+			throw new RuntimeException('The model name you are loading is the name of a resource that is already being used: '.$name);
+		}
+
+		if ($db_conn !== FALSE && ! class_exists('CI_DB', FALSE))
+		{
+			if ($db_conn === TRUE)
+			{
+				$db_conn = '';
+			}
+
+			$this->database($db_conn, FALSE, TRUE);
+		}
+
+		// Note: All of the code under this condition used to be just:
+		//
+		//       load_class('Model', 'core');
+		//
+		//       However, load_class() instantiates classes
+		//       to cache them for later use and that prevents
+		//       MY_Model from being an abstract class and is
+		//       sub-optimal otherwise anyway.
+		if ( ! class_exists('CI_Model', FALSE))
+		{
+			$app_path = APPPATH.'core'.DIRECTORY_SEPARATOR;
+			if (file_exists($app_path.'Model.php'))
+			{
+				require_once($app_path.'Model.php');
+				if ( ! class_exists('CI_Model', FALSE))
+				{
+					throw new RuntimeException($app_path."Model.php exists, but doesn't declare class CI_Model");
+				}
+
+				log_message('info', 'CI_Model class loaded');
+			}
+			elseif ( ! class_exists('CI_Model', FALSE))
+			{
+				require_once(BASEPATH.'core'.DIRECTORY_SEPARATOR.'Model.php');
+			}
+
+			$class = config_item('subclass_prefix').'Model';
+			if (file_exists($app_path.$class.'.php'))
+			{
+				require_once($app_path.$class.'.php');
+				if ( ! class_exists($class, FALSE))
+				{
+					throw new RuntimeException($app_path.$class.".php exists, but doesn't declare class ".$class);
+				}
+
+				log_message('info', config_item('subclass_prefix').'Model class loaded');
+			}
+		}
+
+		$model = ucfirst($model);
+		if ( ! class_exists($model, FALSE))
+		{
+			foreach ($this->_ci_model_paths as $mod_path)
+			{
+				if ( ! file_exists($mod_path.'models/'.$path.$model.'.php'))
+				{
+					continue;
+				}
+
+				require_once($mod_path.'models/'.$path.$model.'.php');
+				if ( ! class_exists($model, FALSE))
+				{
+					throw new RuntimeException($mod_path."models/".$path.$model.".php exists, but doesn't declare class ".$model);
+				}
+
+				break;
+			}
+
+			if ( ! class_exists($model, FALSE))
+			{
+				throw new RuntimeException('Unable to locate the model you have specified: '.$model);
+			}
+		}
+		elseif ( ! is_subclass_of($model, 'CI_Model'))
+		{
+			throw new RuntimeException("Class ".$model." already exists and doesn't extend CI_Model");
+		}
+
+		$this->_ci_models[] = $name;
+		$model = new $model();
+		$CI->$name = $model;
+		log_message('info', 'Model "'.get_class($model).'" initialized');
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database Loader
+	 *
+	 * @param	mixed	$params		Database configuration options
+	 * @param	bool	$return 	Whether to return the database object
+	 * @param	bool	$query_builder	Whether to enable Query Builder
+	 *					(overrides the configuration setting)
+	 *
+	 * @return	object|bool	Database object if $return is set to TRUE,
+	 *					FALSE on failure, CI_Loader instance in any other case
+	 */
+	public function database($params = '', $return = FALSE, $query_builder = NULL)
+	{
+		// Grab the super object
+		$CI =& get_instance();
+
+		// Do we even need to load the database class?
+		if ($return === FALSE && $query_builder === NULL && isset($CI->db) && is_object($CI->db) && ! empty($CI->db->conn_id))
+		{
+			return FALSE;
+		}
+
+		require_once(BASEPATH.'database/DB.php');
+
+		if ($return === TRUE)
+		{
+			return DB($params, $query_builder);
+		}
+
+		// Initialize the db variable. Needed to prevent
+		// reference errors with some configurations
+		$CI->db = '';
+
+		// Load the DB class
+		$CI->db =& DB($params, $query_builder);
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Load the Database Utilities Class
+	 *
+	 * @param	object	$db	Database object
+	 * @param	bool	$return	Whether to return the DB Utilities class object or not
+	 * @return	object
+	 */
+	public function dbutil($db = NULL, $return = FALSE)
+	{
+		$CI =& get_instance();
+
+		if ( ! is_object($db) OR ! ($db instanceof CI_DB))
+		{
+			class_exists('CI_DB', FALSE) OR $this->database();
+			$db =& $CI->db;
+		}
+
+		require_once(BASEPATH.'database/DB_utility.php');
+		require_once(BASEPATH.'database/drivers/'.$db->dbdriver.'/'.$db->dbdriver.'_utility.php');
+		$class = 'CI_DB_'.$db->dbdriver.'_utility';
+
+		if ($return === TRUE)
+		{
+			return new $class($db);
+		}
+
+		$CI->dbutil = new $class($db);
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Load the Database Forge Class
+	 *
+	 * @param	object	$db	Database object
+	 * @param	bool	$return	Whether to return the DB Forge class object or not
+	 * @return	object
+	 */
+	public function dbforge($db = NULL, $return = FALSE)
+	{
+		$CI =& get_instance();
+		if ( ! is_object($db) OR ! ($db instanceof CI_DB))
+		{
+			class_exists('CI_DB', FALSE) OR $this->database();
+			$db =& $CI->db;
+		}
+
+		require_once(BASEPATH.'database/DB_forge.php');
+		require_once(BASEPATH.'database/drivers/'.$db->dbdriver.'/'.$db->dbdriver.'_forge.php');
+
+		if ( ! empty($db->subdriver))
+		{
+			$driver_path = BASEPATH.'database/drivers/'.$db->dbdriver.'/subdrivers/'.$db->dbdriver.'_'.$db->subdriver.'_forge.php';
+			if (file_exists($driver_path))
+			{
+				require_once($driver_path);
+				$class = 'CI_DB_'.$db->dbdriver.'_'.$db->subdriver.'_forge';
+			}
+		}
+		else
+		{
+			$class = 'CI_DB_'.$db->dbdriver.'_forge';
+		}
+
+		if ($return === TRUE)
+		{
+			return new $class($db);
+		}
+
+		$CI->dbforge = new $class($db);
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * View Loader
+	 *
+	 * Loads "view" files.
+	 *
+	 * @param	string	$view	View name
+	 * @param	array	$vars	An associative array of data
+	 *				to be extracted for use in the view
+	 * @param	bool	$return	Whether to return the view output
+	 *				or leave it to the Output class
+	 * @return	object|string
+	 */
+	public function view($view, $vars = array(), $return = FALSE)
+	{
+		return $this->_ci_load(array('_ci_view' => $view, '_ci_vars' => $this->_ci_prepare_view_vars($vars), '_ci_return' => $return));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Generic File Loader
+	 *
+	 * @param	string	$path	File path
+	 * @param	bool	$return	Whether to return the file output
+	 * @return	object|string
+	 */
+	public function file($path, $return = FALSE)
+	{
+		return $this->_ci_load(array('_ci_path' => $path, '_ci_return' => $return));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Variables
+	 *
+	 * Once variables are set they become available within
+	 * the controller class and its "view" files.
+	 *
+	 * @param	array|object|string	$vars
+	 *					An associative array or object containing values
+	 *					to be set, or a value's name if string
+	 * @param 	string	$val	Value to set, only used if $vars is a string
+	 * @return	object
+	 */
+	public function vars($vars, $val = '')
+	{
+		$vars = is_string($vars)
+			? array($vars => $val)
+			: $this->_ci_prepare_view_vars($vars);
+
+		foreach ($vars as $key => $val)
+		{
+			$this->_ci_cached_vars[$key] = $val;
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Clear Cached Variables
+	 *
+	 * Clears the cached variables.
+	 *
+	 * @return	CI_Loader
+	 */
+	public function clear_vars()
+	{
+		$this->_ci_cached_vars = array();
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Variable
+	 *
+	 * Check if a variable is set and retrieve it.
+	 *
+	 * @param	string	$key	Variable name
+	 * @return	mixed	The variable or NULL if not found
+	 */
+	public function get_var($key)
+	{
+		return isset($this->_ci_cached_vars[$key]) ? $this->_ci_cached_vars[$key] : NULL;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Variables
+	 *
+	 * Retrieves all loaded variables.
+	 *
+	 * @return	array
+	 */
+	public function get_vars()
+	{
+		return $this->_ci_cached_vars;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Helper Loader
+	 *
+	 * @param	string|string[]	$helpers	Helper name(s)
+	 * @return	object
+	 */
+	public function helper($helpers = array())
+	{
+		is_array($helpers) OR $helpers = array($helpers);
+		foreach ($helpers as &$helper)
+		{
+			$filename = basename($helper);
+			$filepath = ($filename === $helper) ? '' : substr($helper, 0, strlen($helper) - strlen($filename));
+			$filename = strtolower(preg_replace('#(_helper)?(\.php)?$#i', '', $filename)).'_helper';
+			$helper   = $filepath.$filename;
+
+			if (isset($this->_ci_helpers[$helper]))
+			{
+				continue;
+			}
+
+			// Is this a helper extension request?
+			$ext_helper = config_item('subclass_prefix').$filename;
+			$ext_loaded = FALSE;
+			foreach ($this->_ci_helper_paths as $path)
+			{
+				if (file_exists($path.'helpers/'.$ext_helper.'.php'))
+				{
+					include_once($path.'helpers/'.$ext_helper.'.php');
+					$ext_loaded = TRUE;
+				}
+			}
+
+			// If we have loaded extensions - check if the base one is here
+			if ($ext_loaded === TRUE)
+			{
+				$base_helper = BASEPATH.'helpers/'.$helper.'.php';
+				if ( ! file_exists($base_helper))
+				{
+					show_error('Unable to load the requested file: helpers/'.$helper.'.php');
+				}
+
+				include_once($base_helper);
+				$this->_ci_helpers[$helper] = TRUE;
+				log_message('info', 'Helper loaded: '.$helper);
+				continue;
+			}
+
+			// No extensions found ... try loading regular helpers and/or overrides
+			foreach ($this->_ci_helper_paths as $path)
+			{
+				if (file_exists($path.'helpers/'.$helper.'.php'))
+				{
+					include_once($path.'helpers/'.$helper.'.php');
+
+					$this->_ci_helpers[$helper] = TRUE;
+					log_message('info', 'Helper loaded: '.$helper);
+					break;
+				}
+			}
+
+			// unable to load the helper
+			if ( ! isset($this->_ci_helpers[$helper]))
+			{
+				show_error('Unable to load the requested file: helpers/'.$helper.'.php');
+			}
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Load Helpers
+	 *
+	 * An alias for the helper() method in case the developer has
+	 * written the plural form of it.
+	 *
+	 * @uses	CI_Loader::helper()
+	 * @param	string|string[]	$helpers	Helper name(s)
+	 * @return	object
+	 */
+	public function helpers($helpers = array())
+	{
+		return $this->helper($helpers);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Language Loader
+	 *
+	 * Loads language files.
+	 *
+	 * @param	string|string[]	$files	List of language file names to load
+	 * @param	string		Language name
+	 * @return	object
+	 */
+	public function language($files, $lang = '')
+	{
+		get_instance()->lang->load($files, $lang);
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Config Loader
+	 *
+	 * Loads a config file (an alias for CI_Config::load()).
+	 *
+	 * @uses	CI_Config::load()
+	 * @param	string	$file			Configuration file name
+	 * @param	bool	$use_sections		Whether configuration values should be loaded into their own section
+	 * @param	bool	$fail_gracefully	Whether to just return FALSE or display an error message
+	 * @return	bool	TRUE if the file was loaded correctly or FALSE on failure
+	 */
+	public function config($file, $use_sections = FALSE, $fail_gracefully = FALSE)
+	{
+		return get_instance()->config->load($file, $use_sections, $fail_gracefully);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Driver Loader
+	 *
+	 * Loads a driver library.
+	 *
+	 * @param	string|string[]	$library	Driver name(s)
+	 * @param	array		$params		Optional parameters to pass to the driver
+	 * @param	string		$object_name	An optional object name to assign to
+	 *
+	 * @return	object|bool	Object or FALSE on failure if $library is a string
+	 *				and $object_name is set. CI_Loader instance otherwise.
+	 */
+	public function driver($library, $params = NULL, $object_name = NULL)
+	{
+		if (is_array($library))
+		{
+			foreach ($library as $key => $value)
+			{
+				if (is_int($key))
+				{
+					$this->driver($value, $params);
+				}
+				else
+				{
+					$this->driver($key, $params, $value);
+				}
+			}
+
+			return $this;
+		}
+		elseif (empty($library))
+		{
+			return FALSE;
+		}
+
+		if ( ! class_exists('CI_Driver_Library', FALSE))
+		{
+			// We aren't instantiating an object here, just making the base class available
+			require BASEPATH.'libraries/Driver.php';
+		}
+
+		// We can save the loader some time since Drivers will *always* be in a subfolder,
+		// and typically identically named to the library
+		if ( ! strpos($library, '/'))
+		{
+			$library = ucfirst($library).'/'.$library;
+		}
+
+		return $this->library($library, $params, $object_name);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add Package Path
+	 *
+	 * Prepends a parent path to the library, model, helper and config
+	 * path arrays.
+	 *
+	 * @see	CI_Loader::$_ci_library_paths
+	 * @see	CI_Loader::$_ci_model_paths
+	 * @see CI_Loader::$_ci_helper_paths
+	 * @see CI_Config::$_config_paths
+	 *
+	 * @param	string	$path		Path to add
+	 * @param 	bool	$view_cascade	(default: TRUE)
+	 * @return	object
+	 */
+	public function add_package_path($path, $view_cascade = TRUE)
+	{
+		$path = rtrim($path, '/').'/';
+
+		array_unshift($this->_ci_library_paths, $path);
+		array_unshift($this->_ci_model_paths, $path);
+		array_unshift($this->_ci_helper_paths, $path);
+
+		$this->_ci_view_paths = array($path.'views/' => $view_cascade) + $this->_ci_view_paths;
+
+		// Add config file path
+		$config =& $this->_ci_get_component('config');
+		$config->_config_paths[] = $path;
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Package Paths
+	 *
+	 * Return a list of all package paths.
+	 *
+	 * @param	bool	$include_base	Whether to include BASEPATH (default: FALSE)
+	 * @return	array
+	 */
+	public function get_package_paths($include_base = FALSE)
+	{
+		return ($include_base === TRUE) ? $this->_ci_library_paths : $this->_ci_model_paths;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Remove Package Path
+	 *
+	 * Remove a path from the library, model, helper and/or config
+	 * path arrays if it exists. If no path is provided, the most recently
+	 * added path will be removed removed.
+	 *
+	 * @param	string	$path	Path to remove
+	 * @return	object
+	 */
+	public function remove_package_path($path = '')
+	{
+		$config =& $this->_ci_get_component('config');
+
+		if ($path === '')
+		{
+			array_shift($this->_ci_library_paths);
+			array_shift($this->_ci_model_paths);
+			array_shift($this->_ci_helper_paths);
+			array_shift($this->_ci_view_paths);
+			array_pop($config->_config_paths);
+		}
+		else
+		{
+			$path = rtrim($path, '/').'/';
+			foreach (array('_ci_library_paths', '_ci_model_paths', '_ci_helper_paths') as $var)
+			{
+				if (($key = array_search($path, $this->{$var})) !== FALSE)
+				{
+					unset($this->{$var}[$key]);
+				}
+			}
+
+			if (isset($this->_ci_view_paths[$path.'views/']))
+			{
+				unset($this->_ci_view_paths[$path.'views/']);
+			}
+
+			if (($key = array_search($path, $config->_config_paths)) !== FALSE)
+			{
+				unset($config->_config_paths[$key]);
+			}
+		}
+
+		// make sure the application default paths are still in the array
+		$this->_ci_library_paths = array_unique(array_merge($this->_ci_library_paths, array(APPPATH, BASEPATH)));
+		$this->_ci_helper_paths = array_unique(array_merge($this->_ci_helper_paths, array(APPPATH, BASEPATH)));
+		$this->_ci_model_paths = array_unique(array_merge($this->_ci_model_paths, array(APPPATH)));
+		$this->_ci_view_paths = array_merge($this->_ci_view_paths, array(APPPATH.'views/' => TRUE));
+		$config->_config_paths = array_unique(array_merge($config->_config_paths, array(APPPATH)));
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Internal CI Data Loader
+	 *
+	 * Used to load views and files.
+	 *
+	 * Variables are prefixed with _ci_ to avoid symbol collision with
+	 * variables made available to view files.
+	 *
+	 * @used-by	CI_Loader::view()
+	 * @used-by	CI_Loader::file()
+	 * @param	array	$_ci_data	Data to load
+	 * @return	object
+	 */
+	protected function _ci_load($_ci_data)
+	{
+		// Set the default data variables
+		foreach (array('_ci_view', '_ci_vars', '_ci_path', '_ci_return') as $_ci_val)
+		{
+			$$_ci_val = isset($_ci_data[$_ci_val]) ? $_ci_data[$_ci_val] : FALSE;
+		}
+
+		$file_exists = FALSE;
+
+		// Set the path to the requested file
+		if (is_string($_ci_path) && $_ci_path !== '')
+		{
+			$_ci_x = explode('/', $_ci_path);
+			$_ci_file = end($_ci_x);
+		}
+		else
+		{
+			$_ci_ext = pathinfo($_ci_view, PATHINFO_EXTENSION);
+			$_ci_file = ($_ci_ext === '') ? $_ci_view.'.php' : $_ci_view;
+
+			foreach ($this->_ci_view_paths as $_ci_view_file => $cascade)
+			{
+				if (file_exists($_ci_view_file.$_ci_file))
+				{
+					$_ci_path = $_ci_view_file.$_ci_file;
+					$file_exists = TRUE;
+					break;
+				}
+
+				if ( ! $cascade)
+				{
+					break;
+				}
+			}
+		}
+
+		if ( ! $file_exists && ! file_exists($_ci_path))
+		{
+			show_error('Unable to load the requested file: '.$_ci_file);
+		}
+
+		// This allows anything loaded using $this->load (views, files, etc.)
+		// to become accessible from within the Controller and Model functions.
+		$_ci_CI =& get_instance();
+		foreach (get_object_vars($_ci_CI) as $_ci_key => $_ci_var)
+		{
+			if ( ! isset($this->$_ci_key))
+			{
+				$this->$_ci_key =& $_ci_CI->$_ci_key;
+			}
+		}
+
+		/*
+		 * Extract and cache variables
+		 *
+		 * You can either set variables using the dedicated $this->load->vars()
+		 * function or via the second parameter of this function. We'll merge
+		 * the two types and cache them so that views that are embedded within
+		 * other views can have access to these variables.
+		 */
+		empty($_ci_vars) OR $this->_ci_cached_vars = array_merge($this->_ci_cached_vars, $_ci_vars);
+		extract($this->_ci_cached_vars);
+
+		/*
+		 * Buffer the output
+		 *
+		 * We buffer the output for two reasons:
+		 * 1. Speed. You get a significant speed boost.
+		 * 2. So that the final rendered template can be post-processed by
+		 *	the output class. Why do we need post processing? For one thing,
+		 *	in order to show the elapsed page load time. Unless we can
+		 *	intercept the content right before it's sent to the browser and
+		 *	then stop the timer it won't be accurate.
+		 */
+		ob_start();
+
+		// If the PHP installation does not support short tags we'll
+		// do a little string replacement, changing the short tags
+		// to standard PHP echo statements.
+		if ( ! is_php('5.4') && ! ini_get('short_open_tag') && config_item('rewrite_short_tags') === TRUE)
+		{
+			echo eval('?>'.preg_replace('/;*\s*\?>/', '; ?>', str_replace('<?=', '<?php echo ', file_get_contents($_ci_path))));
+		}
+		else
+		{
+			include($_ci_path); // include() vs include_once() allows for multiple views with the same name
+		}
+
+		log_message('info', 'File loaded: '.$_ci_path);
+
+		// Return the file data if requested
+		if ($_ci_return === TRUE)
+		{
+			$buffer = ob_get_contents();
+			@ob_end_clean();
+			return $buffer;
+		}
+
+		/*
+		 * Flush the buffer... or buff the flusher?
+		 *
+		 * In order to permit views to be nested within
+		 * other views, we need to flush the content back out whenever
+		 * we are beyond the first level of output buffering so that
+		 * it can be seen and included properly by the first included
+		 * template and any subsequent ones. Oy!
+		 */
+		if (ob_get_level() > $this->_ci_ob_level + 1)
+		{
+			ob_end_flush();
+		}
+		else
+		{
+			$_ci_CI->output->append_output(ob_get_contents());
+			@ob_end_clean();
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Internal CI Library Loader
+	 *
+	 * @used-by	CI_Loader::library()
+	 * @uses	CI_Loader::_ci_init_library()
+	 *
+	 * @param	string	$class		Class name to load
+	 * @param	mixed	$params		Optional parameters to pass to the class constructor
+	 * @param	string	$object_name	Optional object name to assign to
+	 * @return	void
+	 */
+	protected function _ci_load_library($class, $params = NULL, $object_name = NULL)
+	{
+		// Get the class name, and while we're at it trim any slashes.
+		// The directory path can be included as part of the class name,
+		// but we don't want a leading slash
+		$class = str_replace('.php', '', trim($class, '/'));
+
+		// Was the path included with the class name?
+		// We look for a slash to determine this
+		if (($last_slash = strrpos($class, '/')) !== FALSE)
+		{
+			// Extract the path
+			$subdir = substr($class, 0, ++$last_slash);
+
+			// Get the filename from the path
+			$class = substr($class, $last_slash);
+		}
+		else
+		{
+			$subdir = '';
+		}
+
+		$class = ucfirst($class);
+
+		// Is this a stock library? There are a few special conditions if so ...
+		if (file_exists(BASEPATH.'libraries/'.$subdir.$class.'.php'))
+		{
+			return $this->_ci_load_stock_library($class, $subdir, $params, $object_name);
+		}
+
+		// Safety: Was the class already loaded by a previous call?
+		if (class_exists($class, FALSE))
+		{
+			$property = $object_name;
+			if (empty($property))
+			{
+				$property = strtolower($class);
+				isset($this->_ci_varmap[$property]) && $property = $this->_ci_varmap[$property];
+			}
+
+			$CI =& get_instance();
+			if (isset($CI->$property))
+			{
+				log_message('debug', $class.' class already loaded. Second attempt ignored.');
+				return;
+			}
+
+			return $this->_ci_init_library($class, '', $params, $object_name);
+		}
+
+		// Let's search for the requested library file and load it.
+		foreach ($this->_ci_library_paths as $path)
+		{
+			// BASEPATH has already been checked for
+			if ($path === BASEPATH)
+			{
+				continue;
+			}
+
+			$filepath = $path.'libraries/'.$subdir.$class.'.php';
+			// Does the file exist? No? Bummer...
+			if ( ! file_exists($filepath))
+			{
+				continue;
+			}
+
+			include_once($filepath);
+			return $this->_ci_init_library($class, '', $params, $object_name);
+		}
+
+		// One last attempt. Maybe the library is in a subdirectory, but it wasn't specified?
+		if ($subdir === '')
+		{
+			return $this->_ci_load_library($class.'/'.$class, $params, $object_name);
+		}
+
+		// If we got this far we were unable to find the requested class.
+		log_message('error', 'Unable to load the requested class: '.$class);
+		show_error('Unable to load the requested class: '.$class);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Internal CI Stock Library Loader
+	 *
+	 * @used-by	CI_Loader::_ci_load_library()
+	 * @uses	CI_Loader::_ci_init_library()
+	 *
+	 * @param	string	$library_name	Library name to load
+	 * @param	string	$file_path	Path to the library filename, relative to libraries/
+	 * @param	mixed	$params		Optional parameters to pass to the class constructor
+	 * @param	string	$object_name	Optional object name to assign to
+	 * @return	void
+	 */
+	protected function _ci_load_stock_library($library_name, $file_path, $params, $object_name)
+	{
+		$prefix = 'CI_';
+
+		if (class_exists($prefix.$library_name, FALSE))
+		{
+			if (class_exists(config_item('subclass_prefix').$library_name, FALSE))
+			{
+				$prefix = config_item('subclass_prefix');
+			}
+
+			$property = $object_name;
+			if (empty($property))
+			{
+				$property = strtolower($library_name);
+				isset($this->_ci_varmap[$property]) && $property = $this->_ci_varmap[$property];
+			}
+
+			$CI =& get_instance();
+			if ( ! isset($CI->$property))
+			{
+				return $this->_ci_init_library($library_name, $prefix, $params, $object_name);
+			}
+
+			log_message('debug', $library_name.' class already loaded. Second attempt ignored.');
+			return;
+		}
+
+		$paths = $this->_ci_library_paths;
+		array_pop($paths); // BASEPATH
+		array_pop($paths); // APPPATH (needs to be the first path checked)
+		array_unshift($paths, APPPATH);
+
+		foreach ($paths as $path)
+		{
+			if (file_exists($path = $path.'libraries/'.$file_path.$library_name.'.php'))
+			{
+				// Override
+				include_once($path);
+				if (class_exists($prefix.$library_name, FALSE))
+				{
+					return $this->_ci_init_library($library_name, $prefix, $params, $object_name);
+				}
+
+				log_message('debug', $path.' exists, but does not declare '.$prefix.$library_name);
+			}
+		}
+
+		include_once(BASEPATH.'libraries/'.$file_path.$library_name.'.php');
+
+		// Check for extensions
+		$subclass = config_item('subclass_prefix').$library_name;
+		foreach ($paths as $path)
+		{
+			if (file_exists($path = $path.'libraries/'.$file_path.$subclass.'.php'))
+			{
+				include_once($path);
+				if (class_exists($subclass, FALSE))
+				{
+					$prefix = config_item('subclass_prefix');
+					break;
+				}
+
+				log_message('debug', $path.' exists, but does not declare '.$subclass);
+			}
+		}
+
+		return $this->_ci_init_library($library_name, $prefix, $params, $object_name);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Internal CI Library Instantiator
+	 *
+	 * @used-by	CI_Loader::_ci_load_stock_library()
+	 * @used-by	CI_Loader::_ci_load_library()
+	 *
+	 * @param	string		$class		Class name
+	 * @param	string		$prefix		Class name prefix
+	 * @param	array|null|bool	$config		Optional configuration to pass to the class constructor:
+	 *						FALSE to skip;
+	 *						NULL to search in config paths;
+	 *						array containing configuration data
+	 * @param	string		$object_name	Optional object name to assign to
+	 * @return	void
+	 */
+	protected function _ci_init_library($class, $prefix, $config = FALSE, $object_name = NULL)
+	{
+		// Is there an associated config file for this class? Note: these should always be lowercase
+		if ($config === NULL)
+		{
+			// Fetch the config paths containing any package paths
+			$config_component = $this->_ci_get_component('config');
+
+			if (is_array($config_component->_config_paths))
+			{
+				$found = FALSE;
+				foreach ($config_component->_config_paths as $path)
+				{
+					// We test for both uppercase and lowercase, for servers that
+					// are case-sensitive with regard to file names. Load global first,
+					// override with environment next
+					if (file_exists($path.'config/'.strtolower($class).'.php'))
+					{
+						include($path.'config/'.strtolower($class).'.php');
+						$found = TRUE;
+					}
+					elseif (file_exists($path.'config/'.ucfirst(strtolower($class)).'.php'))
+					{
+						include($path.'config/'.ucfirst(strtolower($class)).'.php');
+						$found = TRUE;
+					}
+
+					if (file_exists($path.'config/'.ENVIRONMENT.'/'.strtolower($class).'.php'))
+					{
+						include($path.'config/'.ENVIRONMENT.'/'.strtolower($class).'.php');
+						$found = TRUE;
+					}
+					elseif (file_exists($path.'config/'.ENVIRONMENT.'/'.ucfirst(strtolower($class)).'.php'))
+					{
+						include($path.'config/'.ENVIRONMENT.'/'.ucfirst(strtolower($class)).'.php');
+						$found = TRUE;
+					}
+
+					// Break on the first found configuration, thus package
+					// files are not overridden by default paths
+					if ($found === TRUE)
+					{
+						break;
+					}
+				}
+			}
+		}
+
+		$class_name = $prefix.$class;
+
+		// Is the class name valid?
+		if ( ! class_exists($class_name, FALSE))
+		{
+			log_message('error', 'Non-existent class: '.$class_name);
+			show_error('Non-existent class: '.$class_name);
+		}
+
+		// Set the variable name we will assign the class to
+		// Was a custom class name supplied? If so we'll use it
+		if (empty($object_name))
+		{
+			$object_name = strtolower($class);
+			if (isset($this->_ci_varmap[$object_name]))
+			{
+				$object_name = $this->_ci_varmap[$object_name];
+			}
+		}
+
+		// Don't overwrite existing properties
+		$CI =& get_instance();
+		if (isset($CI->$object_name))
+		{
+			if ($CI->$object_name instanceof $class_name)
+			{
+				log_message('debug', $class_name." has already been instantiated as '".$object_name."'. Second attempt aborted.");
+				return;
+			}
+
+			show_error("Resource '".$object_name."' already exists and is not a ".$class_name." instance.");
+		}
+
+		// Save the class name and object name
+		$this->_ci_classes[$object_name] = $class;
+
+		// Instantiate the class
+		$CI->$object_name = isset($config)
+			? new $class_name($config)
+			: new $class_name();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * CI Autoloader
+	 *
+	 * Loads component listed in the config/autoload.php file.
+	 *
+	 * @used-by	CI_Loader::initialize()
+	 * @return	void
+	 */
+	protected function _ci_autoloader()
+	{
+		if (file_exists(APPPATH.'config/autoload.php'))
+		{
+			include(APPPATH.'config/autoload.php');
+		}
+
+		if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/autoload.php'))
+		{
+			include(APPPATH.'config/'.ENVIRONMENT.'/autoload.php');
+		}
+
+		if ( ! isset($autoload))
+		{
+			return;
+		}
+
+		// Autoload packages
+		if (isset($autoload['packages']))
+		{
+			foreach ($autoload['packages'] as $package_path)
+			{
+				$this->add_package_path($package_path);
+			}
+		}
+
+		// Load any custom config file
+		if (count($autoload['config']) > 0)
+		{
+			foreach ($autoload['config'] as $val)
+			{
+				$this->config($val);
+			}
+		}
+
+		// Autoload helpers and languages
+		foreach (array('helper', 'language') as $type)
+		{
+			if (isset($autoload[$type]) && count($autoload[$type]) > 0)
+			{
+				$this->$type($autoload[$type]);
+			}
+		}
+
+		// Autoload drivers
+		if (isset($autoload['drivers']))
+		{
+			$this->driver($autoload['drivers']);
+		}
+
+		// Load libraries
+		if (isset($autoload['libraries']) && count($autoload['libraries']) > 0)
+		{
+			// Load the database driver.
+			if (in_array('database', $autoload['libraries']))
+			{
+				$this->database();
+				$autoload['libraries'] = array_diff($autoload['libraries'], array('database'));
+			}
+
+			// Load all other libraries
+			$this->library($autoload['libraries']);
+		}
+
+		// Autoload models
+		if (isset($autoload['model']))
+		{
+			$this->model($autoload['model']);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Prepare variables for _ci_vars, to be later extract()-ed inside views
+	 *
+	 * Converts objects to associative arrays and filters-out internal
+	 * variable names (i.e. keys prefixed with '_ci_').
+	 *
+	 * @param	mixed	$vars
+	 * @return	array
+	 */
+	protected function _ci_prepare_view_vars($vars)
+	{
+		if ( ! is_array($vars))
+		{
+			$vars = is_object($vars)
+				? get_object_vars($vars)
+				: array();
+		}
+
+		foreach (array_keys($vars) as $key)
+		{
+			if (strncmp($key, '_ci_', 4) === 0)
+			{
+				unset($vars[$key]);
+			}
+		}
+
+		return $vars;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * CI Component getter
+	 *
+	 * Get a reference to a specific library or model.
+	 *
+	 * @param 	string	$component	Component name
+	 * @return	bool
+	 */
+	protected function &_ci_get_component($component)
+	{
+		$CI =& get_instance();
+		return $CI->$component;
+	}
+}
diff --git a/system/core/Log.php b/system/core/Log.php
new file mode 100644
index 0000000..ca3e38a
--- /dev/null
+++ b/system/core/Log.php
@@ -0,0 +1,297 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Logging Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Logging
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/general/errors.html
+ */
+class CI_Log {
+
+	/**
+	 * Path to save log files
+	 *
+	 * @var string
+	 */
+	protected $_log_path;
+
+	/**
+	 * File permissions
+	 *
+	 * @var	int
+	 */
+	protected $_file_permissions = 0644;
+
+	/**
+	 * Level of logging
+	 *
+	 * @var int
+	 */
+	protected $_threshold = 1;
+
+	/**
+	 * Array of threshold levels to log
+	 *
+	 * @var array
+	 */
+	protected $_threshold_array = array();
+
+	/**
+	 * Format of timestamp for log files
+	 *
+	 * @var string
+	 */
+	protected $_date_fmt = 'Y-m-d H:i:s';
+
+	/**
+	 * Filename extension
+	 *
+	 * @var	string
+	 */
+	protected $_file_ext;
+
+	/**
+	 * Whether or not the logger can write to the log files
+	 *
+	 * @var bool
+	 */
+	protected $_enabled = TRUE;
+
+	/**
+	 * Predefined logging levels
+	 *
+	 * @var array
+	 */
+	protected $_levels = array('ERROR' => 1, 'DEBUG' => 2, 'INFO' => 3, 'ALL' => 4);
+
+	/**
+	 * mbstring.func_overload flag
+	 *
+	 * @var	bool
+	 */
+	protected static $func_overload;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		$config =& get_config();
+
+		isset(self::$func_overload) OR self::$func_overload = ( ! is_php('8.0') && extension_loaded('mbstring') && @ini_get('mbstring.func_overload'));
+
+		$this->_log_path = ($config['log_path'] !== '') ? $config['log_path'] : APPPATH.'logs/';
+		$this->_file_ext = (isset($config['log_file_extension']) && $config['log_file_extension'] !== '')
+			? ltrim($config['log_file_extension'], '.') : 'php';
+
+		file_exists($this->_log_path) OR mkdir($this->_log_path, 0755, TRUE);
+
+		if ( ! is_dir($this->_log_path) OR ! is_really_writable($this->_log_path))
+		{
+			$this->_enabled = FALSE;
+		}
+
+		if (is_numeric($config['log_threshold']))
+		{
+			$this->_threshold = (int) $config['log_threshold'];
+		}
+		elseif (is_array($config['log_threshold']))
+		{
+			$this->_threshold = 0;
+			$this->_threshold_array = array_flip($config['log_threshold']);
+		}
+
+		if ( ! empty($config['log_date_format']))
+		{
+			$this->_date_fmt = $config['log_date_format'];
+		}
+
+		if ( ! empty($config['log_file_permissions']) && is_int($config['log_file_permissions']))
+		{
+			$this->_file_permissions = $config['log_file_permissions'];
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Write Log File
+	 *
+	 * Generally this function will be called using the global log_message() function
+	 *
+	 * @param	string	$level 	The error level: 'error', 'debug' or 'info'
+	 * @param	string	$msg 	The error message
+	 * @return	bool
+	 */
+	public function write_log($level, $msg)
+	{
+		if ($this->_enabled === FALSE)
+		{
+			return FALSE;
+		}
+
+		$level = strtoupper($level);
+
+		if (( ! isset($this->_levels[$level]) OR ($this->_levels[$level] > $this->_threshold))
+			&& ! isset($this->_threshold_array[$this->_levels[$level]]))
+		{
+			return FALSE;
+		}
+
+		$filepath = $this->_log_path.'log-'.date('Y-m-d').'.'.$this->_file_ext;
+		$message = '';
+
+		if ( ! file_exists($filepath))
+		{
+			$newfile = TRUE;
+			// Only add protection to php files
+			if ($this->_file_ext === 'php')
+			{
+				$message .= "<?php defined('BASEPATH') OR exit('No direct script access allowed'); ?>\n\n";
+			}
+		}
+
+		if ( ! $fp = @fopen($filepath, 'ab'))
+		{
+			return FALSE;
+		}
+
+		flock($fp, LOCK_EX);
+
+		// Instantiating DateTime with microseconds appended to initial date is needed for proper support of this format
+		if (strpos($this->_date_fmt, 'u') !== FALSE)
+		{
+			$microtime_full = microtime(TRUE);
+			$microtime_short = sprintf("%06d", ($microtime_full - floor($microtime_full)) * 1000000);
+			$date = new DateTime(date('Y-m-d H:i:s.'.$microtime_short, $microtime_full));
+			$date = $date->format($this->_date_fmt);
+		}
+		else
+		{
+			$date = date($this->_date_fmt);
+		}
+
+		$message .= $this->_format_line($level, $date, $msg);
+
+		for ($written = 0, $length = self::strlen($message); $written < $length; $written += $result)
+		{
+			if (($result = fwrite($fp, self::substr($message, $written))) === FALSE)
+			{
+				break;
+			}
+		}
+
+		flock($fp, LOCK_UN);
+		fclose($fp);
+
+		if (isset($newfile) && $newfile === TRUE)
+		{
+			chmod($filepath, $this->_file_permissions);
+		}
+
+		return is_int($result);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Format the log line.
+	 *
+	 * This is for extensibility of log formatting
+	 * If you want to change the log format, extend the CI_Log class and override this method
+	 *
+	 * @param	string	$level 	The error level
+	 * @param	string	$date 	Formatted date string
+	 * @param	string	$message 	The log message
+	 * @return	string	Formatted log line with a new line character at the end
+	 */
+	protected function _format_line($level, $date, $message)
+	{
+		return $level.' - '.$date.' --> '.$message.PHP_EOL;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Byte-safe strlen()
+	 *
+	 * @param	string	$str
+	 * @return	int
+	 */
+	protected static function strlen($str)
+	{
+		return (self::$func_overload)
+			? 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_overload)
+		{
+			// 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/system/core/Model.php b/system/core/Model.php
new file mode 100644
index 0000000..b2bbbd4
--- /dev/null
+++ b/system/core/Model.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Model Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/libraries/config.html
+ */
+class CI_Model {
+
+	/**
+	 * Class constructor
+	 *
+	 * @link	https://github.com/bcit-ci/CodeIgniter/issues/5332
+	 * @return	void
+	 */
+	public function __construct() {}
+
+	/**
+	 * __get magic
+	 *
+	 * Allows models to access CI's loaded classes using the same
+	 * syntax as controllers.
+	 *
+	 * @param	string	$key
+	 */
+	public function __get($key)
+	{
+		// Debugging note:
+		//	If you're here because you're getting an error message
+		//	saying 'Undefined Property: system/core/Model.php', it's
+		//	most likely a typo in your model code.
+		return get_instance()->$key;
+	}
+
+}
diff --git a/system/core/Output.php b/system/core/Output.php
new file mode 100644
index 0000000..a629a09
--- /dev/null
+++ b/system/core/Output.php
@@ -0,0 +1,847 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Output Class
+ *
+ * Responsible for sending final output to the browser.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Output
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/libraries/output.html
+ */
+class CI_Output {
+
+	/**
+	 * Final output string
+	 *
+	 * @var	string
+	 */
+	public $final_output = '';
+
+	/**
+	 * Cache expiration time
+	 *
+	 * @var	int
+	 */
+	public $cache_expiration = 0;
+
+	/**
+	 * List of server headers
+	 *
+	 * @var	array
+	 */
+	public $headers = array();
+
+	/**
+	 * List of mime types
+	 *
+	 * @var	array
+	 */
+	public $mimes =	array();
+
+	/**
+	 * Mime-type for the current page
+	 *
+	 * @var	string
+	 */
+	protected $mime_type = 'text/html';
+
+	/**
+	 * Enable Profiler flag
+	 *
+	 * @var	bool
+	 */
+	public $enable_profiler = FALSE;
+
+	/**
+	 * php.ini zlib.output_compression flag
+	 *
+	 * @var	bool
+	 */
+	protected $_zlib_oc = FALSE;
+
+	/**
+	 * CI output compression flag
+	 *
+	 * @var	bool
+	 */
+	protected $_compress_output = FALSE;
+
+	/**
+	 * List of profiler sections
+	 *
+	 * @var	array
+	 */
+	protected $_profiler_sections =	array();
+
+	/**
+	 * Parse markers flag
+	 *
+	 * Whether or not to parse variables like {elapsed_time} and {memory_usage}.
+	 *
+	 * @var	bool
+	 */
+	public $parse_exec_vars = TRUE;
+
+	/**
+	 * mbstring.func_overload flag
+	 *
+	 * @var	bool
+	 */
+	protected static $func_overload;
+
+	/**
+	 * Class constructor
+	 *
+	 * Determines whether zLib output compression will be used.
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		$this->_zlib_oc = (bool) ini_get('zlib.output_compression');
+		$this->_compress_output = (
+			$this->_zlib_oc === FALSE
+			&& config_item('compress_output') === TRUE
+			&& extension_loaded('zlib')
+		);
+
+		isset(self::$func_overload) OR self::$func_overload = ( ! is_php('8.0') && extension_loaded('mbstring') && @ini_get('mbstring.func_overload'));
+
+		// Get mime types for later
+		$this->mimes =& get_mimes();
+
+		log_message('info', 'Output Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Output
+	 *
+	 * Returns the current output string.
+	 *
+	 * @return	string
+	 */
+	public function get_output()
+	{
+		return $this->final_output;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Output
+	 *
+	 * Sets the output string.
+	 *
+	 * @param	string	$output	Output data
+	 * @return	CI_Output
+	 */
+	public function set_output($output)
+	{
+		$this->final_output = $output;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Append Output
+	 *
+	 * Appends data onto the output string.
+	 *
+	 * @param	string	$output	Data to append
+	 * @return	CI_Output
+	 */
+	public function append_output($output)
+	{
+		$this->final_output .= $output;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Header
+	 *
+	 * Lets you set a server header which will be sent with the final output.
+	 *
+	 * Note: If a file is cached, headers will not be sent.
+	 * @todo	We need to figure out how to permit headers to be cached.
+	 *
+	 * @param	string	$header		Header
+	 * @param	bool	$replace	Whether to replace the old header value, if already set
+	 * @return	CI_Output
+	 */
+	public function set_header($header, $replace = TRUE)
+	{
+		// If zlib.output_compression is enabled it will compress the output,
+		// but it will not modify the content-length header to compensate for
+		// the reduction, causing the browser to hang waiting for more data.
+		// We'll just skip content-length in those cases.
+		if ($this->_zlib_oc && strncasecmp($header, 'content-length', 14) === 0)
+		{
+			return $this;
+		}
+
+		$this->headers[] = array($header, $replace);
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Content-Type Header
+	 *
+	 * @param	string	$mime_type	Extension of the file we're outputting
+	 * @param	string	$charset	Character set (default: NULL)
+	 * @return	CI_Output
+	 */
+	public function set_content_type($mime_type, $charset = NULL)
+	{
+		if (strpos($mime_type, '/') === FALSE)
+		{
+			$extension = ltrim($mime_type, '.');
+
+			// Is this extension supported?
+			if (isset($this->mimes[$extension]))
+			{
+				$mime_type =& $this->mimes[$extension];
+
+				if (is_array($mime_type))
+				{
+					$mime_type = current($mime_type);
+				}
+			}
+		}
+
+		$this->mime_type = $mime_type;
+
+		if (empty($charset))
+		{
+			$charset = config_item('charset');
+		}
+
+		$header = 'Content-Type: '.$mime_type
+			.(empty($charset) ? '' : '; charset='.$charset);
+
+		$this->headers[] = array($header, TRUE);
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Current Content-Type Header
+	 *
+	 * @return	string	'text/html', if not already set
+	 */
+	public function get_content_type()
+	{
+		for ($i = 0, $c = count($this->headers); $i < $c; $i++)
+		{
+			if (sscanf($this->headers[$i][0], 'Content-Type: %[^;]', $content_type) === 1)
+			{
+				return $content_type;
+			}
+		}
+
+		return 'text/html';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Header
+	 *
+	 * @param	string	$header
+	 * @return	string
+	 */
+	public function get_header($header)
+	{
+		// We only need [x][0] from our multi-dimensional array
+		$header_lines = array_map(function ($headers)
+		{
+			return array_shift($headers);
+		}, $this->headers);
+
+		$headers = array_merge(
+			$header_lines,
+			headers_list()
+		);
+
+		if (empty($headers) OR empty($header))
+		{
+			return NULL;
+		}
+
+		// Count backwards, in order to get the last matching header
+		for ($c = count($headers) - 1; $c > -1; $c--)
+		{
+			if (strncasecmp($header, $headers[$c], $l = self::strlen($header)) === 0)
+			{
+				return trim(self::substr($headers[$c], $l+1));
+			}
+		}
+
+		return NULL;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set HTTP Status Header
+	 *
+	 * As of version 1.7.2, this is an alias for common function
+	 * set_status_header().
+	 *
+	 * @param	int	$code	Status code (default: 200)
+	 * @param	string	$text	Optional message
+	 * @return	CI_Output
+	 */
+	public function set_status_header($code = 200, $text = '')
+	{
+		set_status_header($code, $text);
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Enable/disable Profiler
+	 *
+	 * @param	bool	$val	TRUE to enable or FALSE to disable
+	 * @return	CI_Output
+	 */
+	public function enable_profiler($val = TRUE)
+	{
+		$this->enable_profiler = is_bool($val) ? $val : TRUE;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Profiler Sections
+	 *
+	 * Allows override of default/config settings for
+	 * Profiler section display.
+	 *
+	 * @param	array	$sections	Profiler sections
+	 * @return	CI_Output
+	 */
+	public function set_profiler_sections($sections)
+	{
+		if (isset($sections['query_toggle_count']))
+		{
+			$this->_profiler_sections['query_toggle_count'] = (int) $sections['query_toggle_count'];
+			unset($sections['query_toggle_count']);
+		}
+
+		foreach ($sections as $section => $enable)
+		{
+			$this->_profiler_sections[$section] = ($enable !== FALSE);
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Cache
+	 *
+	 * @param	int	$time	Cache expiration time in minutes
+	 * @return	CI_Output
+	 */
+	public function cache($time)
+	{
+		$this->cache_expiration = is_numeric($time) ? $time : 0;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Display Output
+	 *
+	 * Processes and sends finalized output data to the browser along
+	 * with any server headers and profile data. It also stops benchmark
+	 * timers so the page rendering speed and memory usage can be shown.
+	 *
+	 * Note: All "view" data is automatically put into $this->final_output
+	 *	 by controller class.
+	 *
+	 * @uses	CI_Output::$final_output
+	 * @param	string	$output	Output data override
+	 * @return	void
+	 */
+	public function _display($output = '')
+	{
+		// Note:  We use load_class() because we can't use $CI =& get_instance()
+		// since this function is sometimes called by the caching mechanism,
+		// which happens before the CI super object is available.
+		$BM =& load_class('Benchmark', 'core');
+		$CFG =& load_class('Config', 'core');
+
+		// Grab the super object if we can.
+		if (class_exists('CI_Controller', FALSE))
+		{
+			$CI =& get_instance();
+		}
+
+		// --------------------------------------------------------------------
+
+		// Set the output data
+		if ($output === '')
+		{
+			$output =& $this->final_output;
+		}
+
+		// --------------------------------------------------------------------
+
+		// Do we need to write a cache file? Only if the controller does not have its
+		// own _output() method and we are not dealing with a cache file, which we
+		// can determine by the existence of the $CI object above
+		if ($this->cache_expiration > 0 && isset($CI) && ! method_exists($CI, '_output'))
+		{
+			$this->_write_cache($output);
+		}
+
+		// --------------------------------------------------------------------
+
+		// Parse out the elapsed time and memory usage,
+		// then swap the pseudo-variables with the data
+
+		$elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end');
+
+		if ($this->parse_exec_vars === TRUE)
+		{
+			$memory	= round(memory_get_usage() / 1024 / 1024, 2).'MB';
+			$output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output);
+		}
+
+		// --------------------------------------------------------------------
+
+		// Is compression requested?
+		if (isset($CI) // This means that we're not serving a cache file, if we were, it would already be compressed
+			&& $this->_compress_output === TRUE
+			&& isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
+		{
+			ob_start('ob_gzhandler');
+		}
+
+		// --------------------------------------------------------------------
+
+		// Are there any server headers to send?
+		if (count($this->headers) > 0)
+		{
+			foreach ($this->headers as $header)
+			{
+				@header($header[0], $header[1]);
+			}
+		}
+
+		// --------------------------------------------------------------------
+
+		// Does the $CI object exist?
+		// If not we know we are dealing with a cache file so we'll
+		// simply echo out the data and exit.
+		if ( ! isset($CI))
+		{
+			if ($this->_compress_output === TRUE)
+			{
+				if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
+				{
+					header('Content-Encoding: gzip');
+					header('Content-Length: '.self::strlen($output));
+				}
+				else
+				{
+					// User agent doesn't support gzip compression,
+					// so we'll have to decompress our cache
+					$output = gzinflate(self::substr($output, 10, -8));
+				}
+			}
+
+			echo $output;
+			log_message('info', 'Final output sent to browser');
+			log_message('debug', 'Total execution time: '.$elapsed);
+			return;
+		}
+
+		// --------------------------------------------------------------------
+
+		// Do we need to generate profile data?
+		// If so, load the Profile class and run it.
+		if ($this->enable_profiler === TRUE)
+		{
+			$CI->load->library('profiler');
+			if ( ! empty($this->_profiler_sections))
+			{
+				$CI->profiler->set_sections($this->_profiler_sections);
+			}
+
+			// If the output data contains closing </body> and </html> tags
+			// we will remove them and add them back after we insert the profile data
+			$output = preg_replace('|</body>.*?</html>|is', '', $output, -1, $count).$CI->profiler->run();
+			if ($count > 0)
+			{
+				$output .= '</body></html>';
+			}
+		}
+
+		// Does the controller contain a function named _output()?
+		// If so send the output there.  Otherwise, echo it.
+		if (method_exists($CI, '_output'))
+		{
+			$CI->_output($output);
+		}
+		else
+		{
+			echo $output; // Send it to the browser!
+		}
+
+		log_message('info', 'Final output sent to browser');
+		log_message('debug', 'Total execution time: '.$elapsed);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Write Cache
+	 *
+	 * @param	string	$output	Output data to cache
+	 * @return	void
+	 */
+	public function _write_cache($output)
+	{
+		$CI =& get_instance();
+		$path = $CI->config->item('cache_path');
+		$cache_path = ($path === '') ? APPPATH.'cache/' : $path;
+
+		if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path))
+		{
+			log_message('error', 'Unable to write cache file: '.$cache_path);
+			return;
+		}
+
+		$uri = $CI->config->item('base_url')
+			.$CI->config->item('index_page')
+			.$CI->uri->uri_string();
+
+		if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING']))
+		{
+			if (is_array($cache_query_string))
+			{
+				$uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string)));
+			}
+			else
+			{
+				$uri .= '?'.$_SERVER['QUERY_STRING'];
+			}
+		}
+
+		$cache_path .= md5($uri);
+
+		if ( ! $fp = @fopen($cache_path, 'w+b'))
+		{
+			log_message('error', 'Unable to write cache file: '.$cache_path);
+			return;
+		}
+
+		if ( ! flock($fp, LOCK_EX))
+		{
+			log_message('error', 'Unable to secure a file lock for file at: '.$cache_path);
+			fclose($fp);
+			return;
+		}
+
+		// If output compression is enabled, compress the cache
+		// itself, so that we don't have to do that each time
+		// we're serving it
+		if ($this->_compress_output === TRUE)
+		{
+			$output = gzencode($output);
+
+			if ($this->get_header('content-type') === NULL)
+			{
+				$this->set_content_type($this->mime_type);
+			}
+		}
+
+		$expire = time() + ($this->cache_expiration * 60);
+
+		// Put together our serialized info.
+		$cache_info = serialize(array(
+			'expire'	=> $expire,
+			'headers'	=> $this->headers
+		));
+
+		$output = $cache_info.'ENDCI--->'.$output;
+
+		for ($written = 0, $length = self::strlen($output); $written < $length; $written += $result)
+		{
+			if (($result = fwrite($fp, self::substr($output, $written))) === FALSE)
+			{
+				break;
+			}
+		}
+
+		flock($fp, LOCK_UN);
+		fclose($fp);
+
+		if ( ! is_int($result))
+		{
+			@unlink($cache_path);
+			log_message('error', 'Unable to write the complete cache content at: '.$cache_path);
+			return;
+		}
+
+		chmod($cache_path, 0640);
+		log_message('debug', 'Cache file written: '.$cache_path);
+
+		// Send HTTP cache-control headers to browser to match file cache settings.
+		$this->set_cache_header($_SERVER['REQUEST_TIME'], $expire);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update/serve cached output
+	 *
+	 * @uses	CI_Config
+	 * @uses	CI_URI
+	 *
+	 * @param	object	&$CFG	CI_Config class instance
+	 * @param	object	&$URI	CI_URI class instance
+	 * @return	bool	TRUE on success or FALSE on failure
+	 */
+	public function _display_cache(&$CFG, &$URI)
+	{
+		$cache_path = ($CFG->item('cache_path') === '') ? APPPATH.'cache/' : $CFG->item('cache_path');
+
+		// Build the file path. The file name is an MD5 hash of the full URI
+		$uri = $CFG->item('base_url').$CFG->item('index_page').$URI->uri_string;
+
+		if (($cache_query_string = $CFG->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING']))
+		{
+			if (is_array($cache_query_string))
+			{
+				$uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string)));
+			}
+			else
+			{
+				$uri .= '?'.$_SERVER['QUERY_STRING'];
+			}
+		}
+
+		$filepath = $cache_path.md5($uri);
+
+		if ( ! file_exists($filepath) OR ! $fp = @fopen($filepath, 'rb'))
+		{
+			return FALSE;
+		}
+
+		flock($fp, LOCK_SH);
+
+		$cache = (filesize($filepath) > 0) ? fread($fp, filesize($filepath)) : '';
+
+		flock($fp, LOCK_UN);
+		fclose($fp);
+
+		// Look for embedded serialized file info.
+		if ( ! preg_match('/^(.*)ENDCI--->/', $cache, $match))
+		{
+			return FALSE;
+		}
+
+		$cache_info = unserialize($match[1]);
+		$expire = $cache_info['expire'];
+
+		$last_modified = filemtime($filepath);
+
+		// Has the file expired?
+		if ($_SERVER['REQUEST_TIME'] >= $expire && is_really_writable($cache_path))
+		{
+			// If so we'll delete it.
+			@unlink($filepath);
+			log_message('debug', 'Cache file has expired. File deleted.');
+			return FALSE;
+		}
+
+		// Send the HTTP cache control headers
+		$this->set_cache_header($last_modified, $expire);
+
+		// Add headers from cache file.
+		foreach ($cache_info['headers'] as $header)
+		{
+			$this->set_header($header[0], $header[1]);
+		}
+
+		// Display the cache
+		$this->_display(self::substr($cache, self::strlen($match[0])));
+		log_message('debug', 'Cache file is current. Sending it to browser.');
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete cache
+	 *
+	 * @param	string	$uri	URI string
+	 * @return	bool
+	 */
+	public function delete_cache($uri = '')
+	{
+		$CI =& get_instance();
+		$cache_path = $CI->config->item('cache_path');
+		if ($cache_path === '')
+		{
+			$cache_path = APPPATH.'cache/';
+		}
+
+		if ( ! is_dir($cache_path))
+		{
+			log_message('error', 'Unable to find cache path: '.$cache_path);
+			return FALSE;
+		}
+
+		if (empty($uri))
+		{
+			$uri = $CI->uri->uri_string();
+
+			if (($cache_query_string = $CI->config->item('cache_query_string')) && ! empty($_SERVER['QUERY_STRING']))
+			{
+				if (is_array($cache_query_string))
+				{
+					$uri .= '?'.http_build_query(array_intersect_key($_GET, array_flip($cache_query_string)));
+				}
+				else
+				{
+					$uri .= '?'.$_SERVER['QUERY_STRING'];
+				}
+			}
+		}
+
+		$cache_path .= md5($CI->config->item('base_url').$CI->config->item('index_page').ltrim($uri, '/'));
+
+		if ( ! @unlink($cache_path))
+		{
+			log_message('error', 'Unable to delete cache file for '.$uri);
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Cache Header
+	 *
+	 * Set the HTTP headers to match the server-side file cache settings
+	 * in order to reduce bandwidth.
+	 *
+	 * @param	int	$last_modified	Timestamp of when the page was last modified
+	 * @param	int	$expiration	Timestamp of when should the requested page expire from cache
+	 * @return	void
+	 */
+	public function set_cache_header($last_modified, $expiration)
+	{
+		$max_age = $expiration - $_SERVER['REQUEST_TIME'];
+
+		if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $last_modified <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']))
+		{
+			$this->set_status_header(304);
+			exit;
+		}
+
+		header('Pragma: public');
+		header('Cache-Control: max-age='.$max_age.', public');
+		header('Expires: '.gmdate('D, d M Y H:i:s', $expiration).' GMT');
+		header('Last-modified: '.gmdate('D, d M Y H:i:s', $last_modified).' GMT');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Byte-safe strlen()
+	 *
+	 * @param	string	$str
+	 * @return	int
+	 */
+	protected static function strlen($str)
+	{
+		return (self::$func_overload)
+			? 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_overload)
+		{
+			// 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/system/core/Router.php b/system/core/Router.php
new file mode 100644
index 0000000..ab1f44e
--- /dev/null
+++ b/system/core/Router.php
@@ -0,0 +1,516 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Router Class
+ *
+ * Parses URIs and determines routing
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Libraries
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/general/routing.html
+ */
+class CI_Router {
+
+	/**
+	 * CI_Config class object
+	 *
+	 * @var	object
+	 */
+	public $config;
+
+	/**
+	 * List of routes
+	 *
+	 * @var	array
+	 */
+	public $routes =	array();
+
+	/**
+	 * Current class name
+	 *
+	 * @var	string
+	 */
+	public $class =		'';
+
+	/**
+	 * Current method name
+	 *
+	 * @var	string
+	 */
+	public $method =	'index';
+
+	/**
+	 * Sub-directory that contains the requested controller class
+	 *
+	 * @var	string
+	 */
+	public $directory;
+
+	/**
+	 * Default controller (and method if specific)
+	 *
+	 * @var	string
+	 */
+	public $default_controller;
+
+	/**
+	 * Translate URI dashes
+	 *
+	 * Determines whether dashes in controller & method segments
+	 * should be automatically replaced by underscores.
+	 *
+	 * @var	bool
+	 */
+	public $translate_uri_dashes = FALSE;
+
+	/**
+	 * Enable query strings flag
+	 *
+	 * Determines whether to use GET parameters or segment URIs
+	 *
+	 * @var	bool
+	 */
+	public $enable_query_strings = FALSE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Runs the route mapping function.
+	 *
+	 * @param	array	$routing
+	 * @return	void
+	 */
+	public function __construct($routing = NULL)
+	{
+		$this->config =& load_class('Config', 'core');
+		$this->uri =& load_class('URI', 'core');
+
+		$this->enable_query_strings = ( ! is_cli() && $this->config->item('enable_query_strings') === TRUE);
+
+		// If a directory override is configured, it has to be set before any dynamic routing logic
+		is_array($routing) && isset($routing['directory']) && $this->set_directory($routing['directory']);
+		$this->_set_routing();
+
+		// Set any routing overrides that may exist in the main index file
+		if (is_array($routing))
+		{
+			empty($routing['controller']) OR $this->set_class($routing['controller']);
+			empty($routing['function'])   OR $this->set_method($routing['function']);
+		}
+
+		log_message('info', 'Router Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set route mapping
+	 *
+	 * Determines what should be served based on the URI request,
+	 * as well as any "routes" that have been set in the routing config file.
+	 *
+	 * @return	void
+	 */
+	protected function _set_routing()
+	{
+		// Load the routes.php file. It would be great if we could
+		// skip this for enable_query_strings = TRUE, but then
+		// default_controller would be empty ...
+		if (file_exists(APPPATH.'config/routes.php'))
+		{
+			include(APPPATH.'config/routes.php');
+		}
+
+		if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/routes.php'))
+		{
+			include(APPPATH.'config/'.ENVIRONMENT.'/routes.php');
+		}
+
+		// Validate & get reserved routes
+		if (isset($route) && is_array($route))
+		{
+			isset($route['default_controller']) && $this->default_controller = $route['default_controller'];
+			isset($route['translate_uri_dashes']) && $this->translate_uri_dashes = $route['translate_uri_dashes'];
+			unset($route['default_controller'], $route['translate_uri_dashes']);
+			$this->routes = $route;
+		}
+
+		// Are query strings enabled in the config file? Normally CI doesn't utilize query strings
+		// since URI segments are more search-engine friendly, but they can optionally be used.
+		// If this feature is enabled, we will gather the directory/class/method a little differently
+		if ($this->enable_query_strings)
+		{
+			// If the directory is set at this time, it means an override exists, so skip the checks
+			if ( ! isset($this->directory))
+			{
+				$_d = $this->config->item('directory_trigger');
+				$_d = isset($_GET[$_d]) ? trim($_GET[$_d], " \t\n\r\0\x0B/") : '';
+
+				if ($_d !== '')
+				{
+					$this->uri->filter_uri($_d);
+					$this->set_directory($_d);
+				}
+			}
+
+			$_c = trim($this->config->item('controller_trigger'));
+			if ( ! empty($_GET[$_c]))
+			{
+				$this->uri->filter_uri($_GET[$_c]);
+				$this->set_class($_GET[$_c]);
+
+				$_f = trim($this->config->item('function_trigger'));
+				if ( ! empty($_GET[$_f]))
+				{
+					$this->uri->filter_uri($_GET[$_f]);
+					$this->set_method($_GET[$_f]);
+				}
+
+				$this->uri->rsegments = array(
+					1 => $this->class,
+					2 => $this->method
+				);
+			}
+			else
+			{
+				$this->_set_default_controller();
+			}
+
+			// Routing rules don't apply to query strings and we don't need to detect
+			// directories, so we're done here
+			return;
+		}
+
+		// Is there anything to parse?
+		if ($this->uri->uri_string !== '')
+		{
+			$this->_parse_routes();
+		}
+		else
+		{
+			$this->_set_default_controller();
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set request route
+	 *
+	 * Takes an array of URI segments as input and sets the class/method
+	 * to be called.
+	 *
+	 * @used-by	CI_Router::_parse_routes()
+	 * @param	array	$segments	URI segments
+	 * @return	void
+	 */
+	protected function _set_request($segments = array())
+	{
+		$segments = $this->_validate_request($segments);
+		// If we don't have any segments left - try the default controller;
+		// WARNING: Directories get shifted out of the segments array!
+		if (empty($segments))
+		{
+			$this->_set_default_controller();
+			return;
+		}
+
+		if ($this->translate_uri_dashes === TRUE)
+		{
+			$segments[0] = str_replace('-', '_', $segments[0]);
+			if (isset($segments[1]))
+			{
+				$segments[1] = str_replace('-', '_', $segments[1]);
+			}
+		}
+
+		$this->set_class($segments[0]);
+		if (isset($segments[1]))
+		{
+			$this->set_method($segments[1]);
+		}
+		else
+		{
+			$segments[1] = 'index';
+		}
+
+		array_unshift($segments, NULL);
+		unset($segments[0]);
+		$this->uri->rsegments = $segments;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set default controller
+	 *
+	 * @return	void
+	 */
+	protected function _set_default_controller()
+	{
+		if (empty($this->default_controller))
+		{
+			show_error('Unable to determine what should be displayed. A default route has not been specified in the routing file.');
+		}
+
+		// Is the method being specified?
+		if (sscanf($this->default_controller, '%[^/]/%s', $class, $method) !== 2)
+		{
+			$method = 'index';
+		}
+
+		if ( ! file_exists(APPPATH.'controllers/'.$this->directory.ucfirst($class).'.php'))
+		{
+			// This will trigger 404 later
+			return;
+		}
+
+		$this->set_class($class);
+		$this->set_method($method);
+
+		// Assign routed segments, index starting from 1
+		$this->uri->rsegments = array(
+			1 => $class,
+			2 => $method
+		);
+
+		log_message('debug', 'No URI present. Default controller set.');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate request
+	 *
+	 * Attempts validate the URI request and determine the controller path.
+	 *
+	 * @used-by	CI_Router::_set_request()
+	 * @param	array	$segments	URI segments
+	 * @return	mixed	URI segments
+	 */
+	protected function _validate_request($segments)
+	{
+		$c = count($segments);
+		$directory_override = isset($this->directory);
+
+		// Loop through our segments and return as soon as a controller
+		// is found or when such a directory doesn't exist
+		while ($c-- > 0)
+		{
+			$test = $this->directory
+				.ucfirst($this->translate_uri_dashes === TRUE ? str_replace('-', '_', $segments[0]) : $segments[0]);
+
+			if ( ! file_exists(APPPATH.'controllers/'.$test.'.php')
+				&& $directory_override === FALSE
+				&& is_dir(APPPATH.'controllers/'.$this->directory.$segments[0])
+			)
+			{
+				$this->set_directory(array_shift($segments), TRUE);
+				continue;
+			}
+
+			return $segments;
+		}
+
+		// This means that all segments were actually directories
+		return $segments;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse Routes
+	 *
+	 * Matches any routes that may exist in the config/routes.php file
+	 * against the URI to determine if the class/method need to be remapped.
+	 *
+	 * @return	void
+	 */
+	protected function _parse_routes()
+	{
+		// Turn the segment array into a URI string
+		$uri = implode('/', $this->uri->segments);
+
+		// Get HTTP verb
+		$http_verb = isset($_SERVER['REQUEST_METHOD']) ? strtolower($_SERVER['REQUEST_METHOD']) : 'cli';
+
+		// Loop through the route array looking for wildcards
+		foreach ($this->routes as $key => $val)
+		{
+			// Check if route format is using HTTP verbs
+			if (is_array($val))
+			{
+				$val = array_change_key_case($val, CASE_LOWER);
+				if (isset($val[$http_verb]))
+				{
+					$val = $val[$http_verb];
+				}
+				else
+				{
+					continue;
+				}
+			}
+
+			// Convert wildcards to RegEx
+			$key = str_replace(array(':any', ':num'), array('[^/]+', '[0-9]+'), $key);
+
+			// Does the RegEx match?
+			if (preg_match('#^'.$key.'$#', $uri, $matches))
+			{
+				// Are we using callbacks to process back-references?
+				if ( ! is_string($val) && is_callable($val))
+				{
+					// Remove the original string from the matches array.
+					array_shift($matches);
+
+					// Execute the callback using the values in matches as its parameters.
+					$val = call_user_func_array($val, $matches);
+				}
+				// Are we using the default routing method for back-references?
+				elseif (strpos($val, '$') !== FALSE && strpos($key, '(') !== FALSE)
+				{
+					$val = preg_replace('#^'.$key.'$#', $val, $uri);
+				}
+
+				$this->_set_request(explode('/', $val));
+				return;
+			}
+		}
+
+		// If we got this far it means we didn't encounter a
+		// matching route so we'll set the site default route
+		$this->_set_request(array_values($this->uri->segments));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set class name
+	 *
+	 * @param	string	$class	Class name
+	 * @return	void
+	 */
+	public function set_class($class)
+	{
+		$this->class = str_replace(array('/', '.'), '', $class);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch the current class
+	 *
+	 * @deprecated	3.0.0	Read the 'class' property instead
+	 * @return	string
+	 */
+	public function fetch_class()
+	{
+		return $this->class;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set method name
+	 *
+	 * @param	string	$method	Method name
+	 * @return	void
+	 */
+	public function set_method($method)
+	{
+		$this->method = $method;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch the current method
+	 *
+	 * @deprecated	3.0.0	Read the 'method' property instead
+	 * @return	string
+	 */
+	public function fetch_method()
+	{
+		return $this->method;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set directory name
+	 *
+	 * @param	string	$dir	Directory name
+	 * @param	bool	$append	Whether we're appending rather than setting the full value
+	 * @return	void
+	 */
+	public function set_directory($dir, $append = FALSE)
+	{
+		if ($append !== TRUE OR empty($this->directory))
+		{
+			$this->directory = str_replace('.', '', trim($dir, '/')).'/';
+		}
+		else
+		{
+			$this->directory .= str_replace('.', '', trim($dir, '/')).'/';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch directory
+	 *
+	 * Feches the sub-directory (if any) that contains the requested
+	 * controller class.
+	 *
+	 * @deprecated	3.0.0	Read the 'directory' property instead
+	 * @return	string
+	 */
+	public function fetch_directory()
+	{
+		return $this->directory;
+	}
+
+}
diff --git a/system/core/Security.php b/system/core/Security.php
new file mode 100644
index 0000000..e7772e0
--- /dev/null
+++ b/system/core/Security.php
@@ -0,0 +1,1111 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Security Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	Security
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/libraries/security.html
+ */
+class CI_Security {
+
+	/**
+	 * List of sanitize filename strings
+	 *
+	 * @var	array
+	 */
+	public $filename_bad_chars =	array(
+		'../', '<!--', '-->', '<', '>',
+		"'", '"', '&', '$', '#',
+		'{', '}', '[', ']', '=',
+		';', '?', '%20', '%22',
+		'%3c',		// <
+		'%253c',	// <
+		'%3e',		// >
+		'%0e',		// >
+		'%28',		// (
+		'%29',		// )
+		'%2528',	// (
+		'%26',		// &
+		'%24',		// $
+		'%3f',		// ?
+		'%3b',		// ;
+		'%3d'		// =
+	);
+
+	/**
+	 * Character set
+	 *
+	 * Will be overridden by the constructor.
+	 *
+	 * @var	string
+	 */
+	public $charset = 'UTF-8';
+
+	/**
+	 * XSS Hash
+	 *
+	 * Random Hash for protecting URLs.
+	 *
+	 * @var	string
+	 */
+	protected $_xss_hash;
+
+	/**
+	 * CSRF Hash
+	 *
+	 * Random hash for Cross Site Request Forgery protection cookie
+	 *
+	 * @var	string
+	 */
+	protected $_csrf_hash;
+
+	/**
+	 * CSRF Expire time
+	 *
+	 * Expiration time for Cross Site Request Forgery protection cookie.
+	 * Defaults to two hours (in seconds).
+	 *
+	 * @var	int
+	 */
+	protected $_csrf_expire =	7200;
+
+	/**
+	 * CSRF Token name
+	 *
+	 * Token name for Cross Site Request Forgery protection cookie.
+	 *
+	 * @var	string
+	 */
+	protected $_csrf_token_name =	'ci_csrf_token';
+
+	/**
+	 * CSRF Cookie name
+	 *
+	 * Cookie name for Cross Site Request Forgery protection cookie.
+	 *
+	 * @var	string
+	 */
+	protected $_csrf_cookie_name =	'ci_csrf_token';
+
+	/**
+	 * List of never allowed strings
+	 *
+	 * @var	array
+	 */
+	protected $_never_allowed_str =	array(
+		'document.cookie' => '[removed]',
+		'(document).cookie' => '[removed]',
+		'document.write'  => '[removed]',
+		'(document).write'  => '[removed]',
+		'.parentNode'     => '[removed]',
+		'.innerHTML'      => '[removed]',
+		'-moz-binding'    => '[removed]',
+		'<!--'            => '&lt;!--',
+		'-->'             => '--&gt;',
+		'<![CDATA['       => '&lt;![CDATA[',
+		'<comment>'	  => '&lt;comment&gt;',
+		'<%'              => '&lt;&#37;'
+	);
+
+	/**
+	 * List of never allowed regex replacements
+	 *
+	 * @var	array
+	 */
+	protected $_never_allowed_regex = array(
+		'javascript\s*:',
+		'(\(?document\)?|\(?window\)?(\.document)?)\.(location|on\w*)',
+		'expression\s*(\(|&\#40;)', // CSS and IE
+		'vbscript\s*:', // IE, surprise!
+		'wscript\s*:', // IE
+		'jscript\s*:', // IE
+		'vbs\s*:', // IE
+		'Redirect\s+30\d',
+		"([\"'])?data\s*:[^\\1]*?base64[^\\1]*?,[^\\1]*?\\1?"
+	);
+
+	/**
+	 * Class constructor
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		// Is CSRF protection enabled?
+		if (config_item('csrf_protection'))
+		{
+			// CSRF config
+			foreach (array('csrf_expire', 'csrf_token_name', 'csrf_cookie_name') as $key)
+			{
+				if (NULL !== ($val = config_item($key)))
+				{
+					$this->{'_'.$key} = $val;
+				}
+			}
+
+			// Append application specific cookie prefix
+			if ($cookie_prefix = config_item('cookie_prefix'))
+			{
+				$this->_csrf_cookie_name = $cookie_prefix.$this->_csrf_cookie_name;
+			}
+
+			// Set the CSRF hash
+			$this->_csrf_set_hash();
+		}
+
+		$this->charset = strtoupper((string) config_item('charset'));
+
+		log_message('info', 'Security Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * CSRF Verify
+	 *
+	 * @return	CI_Security
+	 */
+	public function csrf_verify()
+	{
+		// If it's not a POST request we will set the CSRF cookie
+		if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST')
+		{
+			return $this->csrf_set_cookie();
+		}
+
+		// Check if URI has been whitelisted from CSRF checks
+		if ($exclude_uris = config_item('csrf_exclude_uris'))
+		{
+			$uri = load_class('URI', 'core');
+			foreach ($exclude_uris as $excluded)
+			{
+				if (preg_match('#^'.$excluded.'$#i'.(UTF8_ENABLED ? 'u' : ''), $uri->uri_string()))
+				{
+					return $this;
+				}
+			}
+		}
+
+		// Check CSRF token validity, but don't error on mismatch just yet - we'll want to regenerate
+		$valid = isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name])
+			&& is_string($_POST[$this->_csrf_token_name]) && is_string($_COOKIE[$this->_csrf_cookie_name])
+			&& hash_equals($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name]);
+
+		// We kill this since we're done and we don't want to pollute the _POST array
+		unset($_POST[$this->_csrf_token_name]);
+
+		// Regenerate on every submission?
+		if (config_item('csrf_regenerate'))
+		{
+			// Nothing should last forever
+			unset($_COOKIE[$this->_csrf_cookie_name]);
+			$this->_csrf_hash = NULL;
+		}
+
+		$this->_csrf_set_hash();
+		$this->csrf_set_cookie();
+
+		if ($valid !== TRUE)
+		{
+			$this->csrf_show_error();
+		}
+
+		log_message('info', 'CSRF token verified');
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * CSRF Set Cookie
+	 *
+	 * @codeCoverageIgnore
+	 * @return	CI_Security
+	 */
+	public function csrf_set_cookie()
+	{
+		$expire = time() + $this->_csrf_expire;
+		$secure_cookie = (bool) config_item('cookie_secure');
+
+		if ($secure_cookie && ! is_https())
+		{
+			return FALSE;
+		}
+
+		if (is_php('7.3'))
+		{
+			setcookie(
+				$this->_csrf_cookie_name,
+				$this->_csrf_hash,
+				array(
+					'expires'  => $expire,
+					'path'     => config_item('cookie_path'),
+					'domain'   => config_item('cookie_domain'),
+					'secure'   => $secure_cookie,
+					'httponly' => config_item('cookie_httponly'),
+					'samesite' => 'Strict'
+				)
+			);
+		}
+		else
+		{
+			$domain = trim(config_item('cookie_domain'));
+			header('Set-Cookie: '.$this->_csrf_cookie_name.'='.$this->_csrf_hash
+					.'; Expires='.gmdate('D, d-M-Y H:i:s T', $expire)
+					.'; Max-Age='.$this->_csrf_expire
+					.'; Path='.rawurlencode(config_item('cookie_path'))
+					.($domain === '' ? '' : '; Domain='.$domain)
+					.($secure_cookie ? '; Secure' : '')
+					.(config_item('cookie_httponly') ? '; HttpOnly' : '')
+					.'; SameSite=Strict'
+			);
+		}
+
+		log_message('info', 'CSRF cookie sent');
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show CSRF Error
+	 *
+	 * @return	void
+	 */
+	public function csrf_show_error()
+	{
+		show_error('The action you have requested is not allowed.', 403);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get CSRF Hash
+	 *
+	 * @see		CI_Security::$_csrf_hash
+	 * @return 	string	CSRF hash
+	 */
+	public function get_csrf_hash()
+	{
+		return $this->_csrf_hash;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get CSRF Token Name
+	 *
+	 * @see		CI_Security::$_csrf_token_name
+	 * @return	string	CSRF token name
+	 */
+	public function get_csrf_token_name()
+	{
+		return $this->_csrf_token_name;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * XSS Clean
+	 *
+	 * Sanitizes data so that Cross Site Scripting Hacks can be
+	 * prevented.  This method does a fair amount of work but
+	 * it is extremely thorough, designed to prevent even the
+	 * most obscure XSS attempts.  Nothing is ever 100% foolproof,
+	 * of course, but I haven't been able to get anything passed
+	 * the filter.
+	 *
+	 * Note: Should only be used to deal with data upon submission.
+	 *	 It's not something that should be used for general
+	 *	 runtime processing.
+	 *
+	 * @link	http://channel.bitflux.ch/wiki/XSS_Prevention
+	 * 		Based in part on some code and ideas from Bitflux.
+	 *
+	 * @link	http://ha.ckers.org/xss.html
+	 * 		To help develop this script I used this great list of
+	 *		vulnerabilities along with a few other hacks I've
+	 *		harvested from examining vulnerabilities in other programs.
+	 *
+	 * @param	string|string[]	$str		Input data
+	 * @param 	bool		$is_image	Whether the input is an image
+	 * @return	string
+	 */
+	public function xss_clean($str, $is_image = FALSE)
+	{
+		// Is the string an array?
+		if (is_array($str))
+		{
+			foreach ($str as $key => &$value)
+			{
+				$str[$key] = $this->xss_clean($value);
+			}
+
+			return $str;
+		}
+
+		// Remove Invisible Characters
+		$str = remove_invisible_characters($str);
+
+		/*
+		 * URL Decode
+		 *
+		 * Just in case stuff like this is submitted:
+		 *
+		 * <a href="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">Google</a>
+		 *
+		 * Note: Use rawurldecode() so it does not remove plus signs
+		 */
+		if (stripos($str, '%') !== false)
+		{
+			do
+			{
+				$oldstr = $str;
+				$str = rawurldecode($str);
+				$str = preg_replace_callback('#%(?:\s*[0-9a-f]){2,}#i', array($this, '_urldecodespaces'), $str);
+			}
+			while ($oldstr !== $str);
+			unset($oldstr);
+		}
+
+		/*
+		 * Convert character entities to ASCII
+		 *
+		 * This permits our tests below to work reliably.
+		 * We only convert entities that are within tags since
+		 * these are the ones that will pose security problems.
+		 */
+		$str = preg_replace_callback("/[^a-z0-9>]+[a-z0-9]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str);
+		$str = preg_replace_callback('/<\w+.*/si', array($this, '_decode_entity'), $str);
+
+		// Remove Invisible Characters Again!
+		$str = remove_invisible_characters($str);
+
+		/*
+		 * Convert all tabs to spaces
+		 *
+		 * This prevents strings like this: ja	vascript
+		 * NOTE: we deal with spaces between characters later.
+		 * NOTE: preg_replace was found to be amazingly slow here on
+		 * large blocks of data, so we use str_replace.
+		 */
+		$str = str_replace("\t", ' ', $str);
+
+		// Capture converted string for later comparison
+		$converted_string = $str;
+
+		// Remove Strings that are never allowed
+		$str = $this->_do_never_allowed($str);
+
+		/*
+		 * Makes PHP tags safe
+		 *
+		 * Note: XML tags are inadvertently replaced too:
+		 *
+		 * <?xml
+		 *
+		 * But it doesn't seem to pose a problem.
+		 */
+		if ($is_image === TRUE)
+		{
+			// Images have a tendency to have the PHP short opening and
+			// closing tags every so often so we skip those and only
+			// do the long opening tags.
+			$str = preg_replace('/<\?(php)/i', '&lt;?\\1', $str);
+		}
+		else
+		{
+			$str = str_replace(array('<?', '?'.'>'), array('&lt;?', '?&gt;'), $str);
+		}
+
+		/*
+		 * Compact any exploded words
+		 *
+		 * This corrects words like:  j a v a s c r i p t
+		 * These words are compacted back to their correct state.
+		 */
+		$words = array(
+			'javascript', 'expression', 'vbscript', 'jscript', 'wscript',
+			'vbs', 'script', 'base64', 'applet', 'alert', 'document',
+			'write', 'cookie', 'window', 'confirm', 'prompt', 'eval'
+		);
+
+		foreach ($words as $word)
+		{
+			$word = implode('\s*', str_split($word)).'\s*';
+
+			// We only want to do this when it is followed by a non-word character
+			// That way valid stuff like "dealer to" does not become "dealerto"
+			$str = preg_replace_callback('#('.substr($word, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str);
+		}
+
+		/*
+		 * Remove disallowed Javascript in links or img tags
+		 * We used to do some version comparisons and use of stripos(),
+		 * but it is dog slow compared to these simplified non-capturing
+		 * preg_match(), especially if the pattern exists in the string
+		 *
+		 * Note: It was reported that not only space characters, but all in
+		 * the following pattern can be parsed as separators between a tag name
+		 * and its attributes: [\d\s"\'`;,\/\=\(\x00\x0B\x09\x0C]
+		 * ... however, remove_invisible_characters() above already strips the
+		 * hex-encoded ones, so we'll skip them below.
+		 */
+		do
+		{
+			$original = $str;
+
+			if (preg_match('/<a/i', $str))
+			{
+				$str = preg_replace_callback('#<a(?:rea)?[^a-z0-9>]+([^>]*?)(?:>|$)#si', array($this, '_js_link_removal'), $str);
+			}
+
+			if (preg_match('/<img/i', $str))
+			{
+				$str = preg_replace_callback('#<img[^a-z0-9]+([^>]*?)(?:\s?/?>|$)#si', array($this, '_js_img_removal'), $str);
+			}
+
+			if (preg_match('/script|xss/i', $str))
+			{
+				$str = preg_replace('#</*(?:script|xss).*?>#si', '[removed]', $str);
+			}
+		}
+		while ($original !== $str);
+		unset($original);
+
+		/*
+		 * Sanitize naughty HTML elements
+		 *
+		 * If a tag containing any of the words in the list
+		 * below is found, the tag gets converted to entities.
+		 *
+		 * So this: <blink>
+		 * Becomes: &lt;blink&gt;
+		 */
+		$pattern = '#'
+			.'<((?<slash>/*\s*)((?<tagName>[a-z0-9]+)(?=[^a-z0-9]|$)|.+)' // tag start and name, followed by a non-tag character
+			.'[^\s\042\047a-z0-9>/=]*' // a valid attribute character immediately after the tag would count as a separator
+			// optional attributes
+			.'(?<attributes>(?:[\s\042\047/=]*' // non-attribute characters, excluding > (tag close) for obvious reasons
+			.'[^\s\042\047>/=]+' // attribute characters
+			// optional attribute-value
+				.'(?:\s*=' // attribute-value separator
+					.'(?:[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*))' // single, double or non-quoted value
+				.')?' // end optional attribute-value group
+			.')*)' // end optional attributes group
+			.'[^>]*)(?<closeTag>\>)?#isS';
+
+		// Note: It would be nice to optimize this for speed, BUT
+		//       only matching the naughty elements here results in
+		//       false positives and in turn - vulnerabilities!
+		do
+		{
+			$old_str = $str;
+			$str = preg_replace_callback($pattern, array($this, '_sanitize_naughty_html'), $str);
+		}
+		while ($old_str !== $str);
+		unset($old_str);
+
+		/*
+		 * Sanitize naughty scripting elements
+		 *
+		 * Similar to above, only instead of looking for
+		 * tags it looks for PHP and JavaScript commands
+		 * that are disallowed. Rather than removing the
+		 * code, it simply converts the parenthesis to entities
+		 * rendering the code un-executable.
+		 *
+		 * For example:	eval('some code')
+		 * Becomes:	eval&#40;'some code'&#41;
+		 */
+		$str = preg_replace(
+			'#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si',
+			'\\1\\2&#40;\\3&#41;',
+			$str
+		);
+
+		// Same thing, but for "tag functions" (e.g. eval`some code`)
+		// See https://github.com/bcit-ci/CodeIgniter/issues/5420
+		$str = preg_replace(
+			'#(alert|prompt|confirm|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)`(.*?)`#si',
+			'\\1\\2&#96;\\3&#96;',
+			$str
+		);
+
+		// Final clean up
+		// This adds a bit of extra precaution in case
+		// something got through the above filters
+		$str = $this->_do_never_allowed($str);
+
+		/*
+		 * Images are Handled in a Special Way
+		 * - Essentially, we want to know that after all of the character
+		 * conversion is done whether any unwanted, likely XSS, code was found.
+		 * If not, we return TRUE, as the image is clean.
+		 * However, if the string post-conversion does not matched the
+		 * string post-removal of XSS, then it fails, as there was unwanted XSS
+		 * code found and removed/changed during processing.
+		 */
+		if ($is_image === TRUE)
+		{
+			return ($str === $converted_string);
+		}
+
+		return $str;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * XSS Hash
+	 *
+	 * Generates the XSS hash if needed and returns it.
+	 *
+	 * @see		CI_Security::$_xss_hash
+	 * @return	string	XSS hash
+	 */
+	public function xss_hash()
+	{
+		if ($this->_xss_hash === NULL)
+		{
+			$rand = $this->get_random_bytes(16);
+			$this->_xss_hash = ($rand === FALSE)
+				? md5(uniqid(mt_rand(), TRUE))
+				: bin2hex($rand);
+		}
+
+		return $this->_xss_hash;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get random bytes
+	 *
+	 * @param	int	$length	Output length
+	 * @return	string
+	 */
+	public function get_random_bytes($length)
+	{
+		if (empty($length) OR ! ctype_digit((string) $length))
+		{
+			return FALSE;
+		}
+
+		if (function_exists('random_bytes'))
+		{
+			try
+			{
+				// The cast is required to avoid TypeError
+				return random_bytes((int) $length);
+			}
+			catch (Exception $e)
+			{
+				// If random_bytes() can't do the job, we can't either ...
+				// There's no point in using fallbacks.
+				log_message('error', $e->getMessage());
+				return FALSE;
+			}
+		}
+
+		// Unfortunately, none of the following PRNGs is guaranteed to exist ...
+		if (defined('MCRYPT_DEV_URANDOM') && ($output = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM)) !== FALSE)
+		{
+			return $output;
+		}
+
+		if (is_readable('/dev/urandom') && ($fp = fopen('/dev/urandom', 'rb')) !== FALSE)
+		{
+			// Try not to waste entropy ...
+			is_php('5.4') && stream_set_chunk_size($fp, $length);
+			$output = fread($fp, $length);
+			fclose($fp);
+			if ($output !== FALSE)
+			{
+				return $output;
+			}
+		}
+
+		if (function_exists('openssl_random_pseudo_bytes'))
+		{
+			return openssl_random_pseudo_bytes($length);
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * HTML Entities Decode
+	 *
+	 * A replacement for html_entity_decode()
+	 *
+	 * The reason we are not using html_entity_decode() by itself is because
+	 * while it is not technically correct to leave out the semicolon
+	 * at the end of an entity most browsers will still interpret the entity
+	 * correctly. html_entity_decode() does not convert entities without
+	 * semicolons, so we are left with our own little solution here. Bummer.
+	 *
+	 * @link	http://php.net/html-entity-decode
+	 *
+	 * @param	string	$str		Input
+	 * @param	string	$charset	Character set
+	 * @return	string
+	 */
+	public function entity_decode($str, $charset = NULL)
+	{
+		if (strpos($str, '&') === FALSE)
+		{
+			return $str;
+		}
+
+		static $_entities;
+
+		isset($charset) OR $charset = $this->charset;
+		$flag = is_php('5.4')
+			? ENT_COMPAT | ENT_HTML5
+			: ENT_COMPAT;
+
+		if ( ! isset($_entities))
+		{
+			$_entities = array_map('strtolower', get_html_translation_table(HTML_ENTITIES, $flag, $charset));
+
+			// If we're not on PHP 5.4+, add the possibly dangerous HTML 5
+			// entities to the array manually
+			if ($flag === ENT_COMPAT)
+			{
+				$_entities[':'] = '&colon;';
+				$_entities['('] = '&lpar;';
+				$_entities[')'] = '&rpar;';
+				$_entities["\n"] = '&NewLine;';
+				$_entities["\t"] = '&Tab;';
+			}
+		}
+
+		do
+		{
+			$str_compare = $str;
+
+			// Decode standard entities, avoiding false positives
+			if (preg_match_all('/&[a-z]{2,}(?![a-z;])/i', $str, $matches))
+			{
+				$replace = array();
+				$matches = array_unique(array_map('strtolower', $matches[0]));
+				foreach ($matches as &$match)
+				{
+					if (($char = array_search($match.';', $_entities, TRUE)) !== FALSE)
+					{
+						$replace[$match] = $char;
+					}
+				}
+
+				$str = str_replace(array_keys($replace), array_values($replace), $str);
+			}
+
+			// Decode numeric & UTF16 two byte entities
+			$str = html_entity_decode(
+				preg_replace('/(&#(?:x0*[0-9a-f]{2,5}(?![0-9a-f;])|(?:0*\d{2,4}(?![0-9;]))))/iS', '$1;', $str),
+				$flag,
+				$charset
+			);
+
+			if ($flag === ENT_COMPAT)
+			{
+				$str = str_replace(array_values($_entities), array_keys($_entities), $str);
+			}
+		}
+		while ($str_compare !== $str);
+		return $str;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Sanitize Filename
+	 *
+	 * @param	string	$str		Input file name
+	 * @param 	bool	$relative_path	Whether to preserve paths
+	 * @return	string
+	 */
+	public function sanitize_filename($str, $relative_path = FALSE)
+	{
+		$bad = $this->filename_bad_chars;
+
+		if ( ! $relative_path)
+		{
+			$bad[] = './';
+			$bad[] = '/';
+		}
+
+		$str = remove_invisible_characters($str, FALSE);
+
+		do
+		{
+			$old = $str;
+			$str = str_replace($bad, '', $str);
+		}
+		while ($old !== $str);
+
+		return stripslashes($str);
+	}
+
+	// ----------------------------------------------------------------
+
+	/**
+	 * Strip Image Tags
+	 *
+	 * @param	string	$str
+	 * @return	string
+	 */
+	public function strip_image_tags($str)
+	{
+		return preg_replace(
+			array(
+				'#<img[\s/]+.*?src\s*=\s*(["\'])([^\\1]+?)\\1.*?\>#i',
+				'#<img[\s/]+.*?src\s*=\s*?(([^\s"\'=<>`]+)).*?\>#i'
+			),
+			'\\2',
+			$str
+		);
+	}
+
+	// ----------------------------------------------------------------
+
+	/**
+	 * URL-decode taking spaces into account
+	 *
+	 * @see		https://github.com/bcit-ci/CodeIgniter/issues/4877
+	 * @param	array	$matches
+	 * @return	string
+	 */
+	protected function _urldecodespaces($matches)
+	{
+		$input    = $matches[0];
+		$nospaces = preg_replace('#\s+#', '', $input);
+		return ($nospaces === $input)
+			? $input
+			: rawurldecode($nospaces);
+	}
+
+	// ----------------------------------------------------------------
+
+	/**
+	 * Compact Exploded Words
+	 *
+	 * Callback method for xss_clean() to remove whitespace from
+	 * things like 'j a v a s c r i p t'.
+	 *
+	 * @used-by	CI_Security::xss_clean()
+	 * @param	array	$matches
+	 * @return	string
+	 */
+	protected function _compact_exploded_words($matches)
+	{
+		return preg_replace('/\s+/s', '', $matches[1]).$matches[2];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Sanitize Naughty HTML
+	 *
+	 * Callback method for xss_clean() to remove naughty HTML elements.
+	 *
+	 * @used-by	CI_Security::xss_clean()
+	 * @param	array	$matches
+	 * @return	string
+	 */
+	protected function _sanitize_naughty_html($matches)
+	{
+		static $naughty_tags    = array(
+			'alert', 'area', 'prompt', 'confirm', 'applet', 'audio', 'basefont', 'base', 'behavior', 'bgsound',
+			'blink', 'body', 'embed', 'expression', 'form', 'frameset', 'frame', 'head', 'html', 'ilayer',
+			'iframe', 'input', 'button', 'select', 'isindex', 'layer', 'link', 'meta', 'keygen', 'object',
+			'plaintext', 'style', 'script', 'textarea', 'title', 'math', 'video', 'svg', 'xml', 'xss'
+		);
+
+		static $evil_attributes = array(
+			'on\w+', 'style', 'xmlns', 'formaction', 'form', 'xlink:href', 'FSCommand', 'seekSegmentTime'
+		);
+
+		// First, escape unclosed tags
+		if (empty($matches['closeTag']))
+		{
+			return '&lt;'.$matches[1];
+		}
+		// Is the element that we caught naughty? If so, escape it
+		elseif (in_array(strtolower($matches['tagName']), $naughty_tags, TRUE))
+		{
+			return '&lt;'.$matches[1].'&gt;';
+		}
+		// For other tags, see if their attributes are "evil" and strip those
+		elseif (isset($matches['attributes']))
+		{
+			// We'll store the already filtered attributes here
+			$attributes = array();
+
+			// Attribute-catching pattern
+			$attributes_pattern = '#'
+				.'(?<name>[^\s\042\047>/=]+)' // attribute characters
+				// optional attribute-value
+				.'(?:\s*=(?<value>[^\s\042\047=><`]+|\s*\042[^\042]*\042|\s*\047[^\047]*\047|\s*(?U:[^\s\042\047=><`]*)))' // attribute-value separator
+				.'#i';
+
+			// Blacklist pattern for evil attribute names
+			$is_evil_pattern = '#^('.implode('|', $evil_attributes).')$#i';
+
+			// Each iteration filters a single attribute
+			do
+			{
+				// Strip any non-alpha characters that may precede an attribute.
+				// Browsers often parse these incorrectly and that has been a
+				// of numerous XSS issues we've had.
+				$matches['attributes'] = preg_replace('#^[^a-z]+#i', '', $matches['attributes']);
+
+				if ( ! preg_match($attributes_pattern, $matches['attributes'], $attribute, PREG_OFFSET_CAPTURE))
+				{
+					// No (valid) attribute found? Discard everything else inside the tag
+					break;
+				}
+
+				if (
+					// Is it indeed an "evil" attribute?
+					preg_match($is_evil_pattern, $attribute['name'][0])
+					// Or does it have an equals sign, but no value and not quoted? Strip that too!
+					OR (trim($attribute['value'][0]) === '')
+				)
+				{
+					$attributes[] = 'xss=removed';
+				}
+				else
+				{
+					$attributes[] = $attribute[0][0];
+				}
+
+				$matches['attributes'] = substr($matches['attributes'], $attribute[0][1] + strlen($attribute[0][0]));
+			}
+			while ($matches['attributes'] !== '');
+
+			$attributes = empty($attributes)
+				? ''
+				: ' '.implode(' ', $attributes);
+			return '<'.$matches['slash'].$matches['tagName'].$attributes.'>';
+		}
+
+		return $matches[0];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * JS Link Removal
+	 *
+	 * Callback method for xss_clean() to sanitize links.
+	 *
+	 * This limits the PCRE backtracks, making it more performance friendly
+	 * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
+	 * PHP 5.2+ on link-heavy strings.
+	 *
+	 * @used-by	CI_Security::xss_clean()
+	 * @param	array	$match
+	 * @return	string
+	 */
+	protected function _js_link_removal($match)
+	{
+		return str_replace(
+			$match[1],
+			preg_replace(
+				'#href=.*?(?:(?:alert|prompt|confirm)(?:\(|&\#40;|`|&\#96;)|javascript:|livescript:|mocha:|charset=|window\.|\(?document\)?\.|\.cookie|<script|<xss|d\s*a\s*t\s*a\s*:)#si',
+				'',
+				$this->_filter_attributes($match[1])
+			),
+			$match[0]
+		);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * JS Image Removal
+	 *
+	 * Callback method for xss_clean() to sanitize image tags.
+	 *
+	 * This limits the PCRE backtracks, making it more performance friendly
+	 * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in
+	 * PHP 5.2+ on image tag heavy strings.
+	 *
+	 * @used-by	CI_Security::xss_clean()
+	 * @param	array	$match
+	 * @return	string
+	 */
+	protected function _js_img_removal($match)
+	{
+		return str_replace(
+			$match[1],
+			preg_replace(
+				'#src=.*?(?:(?:alert|prompt|confirm|eval)(?:\(|&\#40;|`|&\#96;)|javascript:|livescript:|mocha:|charset=|window\.|\(?document\)?\.|\.cookie|<script|<xss|base64\s*,)#si',
+				'',
+				$this->_filter_attributes($match[1])
+			),
+			$match[0]
+		);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Attribute Conversion
+	 *
+	 * @used-by	CI_Security::xss_clean()
+	 * @param	array	$match
+	 * @return	string
+	 */
+	protected function _convert_attribute($match)
+	{
+		return str_replace(array('>', '<', '\\'), array('&gt;', '&lt;', '\\\\'), $match[0]);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Filter Attributes
+	 *
+	 * Filters tag attributes for consistency and safety.
+	 *
+	 * @used-by	CI_Security::_js_img_removal()
+	 * @used-by	CI_Security::_js_link_removal()
+	 * @param	string	$str
+	 * @return	string
+	 */
+	protected function _filter_attributes($str)
+	{
+		$out = '';
+		if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches))
+		{
+			foreach ($matches[0] as $match)
+			{
+				$out .= preg_replace('#/\*.*?\*/#s', '', $match);
+			}
+		}
+
+		return $out;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * HTML Entity Decode Callback
+	 *
+	 * @used-by	CI_Security::xss_clean()
+	 * @param	array	$match
+	 * @return	string
+	 */
+	protected function _decode_entity($match)
+	{
+		// Protect GET variables in URLs
+		// 901119URL5918AMP18930PROTECT8198
+		$match = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-/]+)|i', $this->xss_hash().'\\1=\\2', $match[0]);
+
+		// Decode, then un-protect URL GET vars
+		return str_replace(
+			$this->xss_hash(),
+			'&',
+			$this->entity_decode($match, $this->charset)
+		);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Do Never Allowed
+	 *
+	 * @used-by	CI_Security::xss_clean()
+	 * @param 	string
+	 * @return 	string
+	 */
+	protected function _do_never_allowed($str)
+	{
+		$str = str_replace(array_keys($this->_never_allowed_str), $this->_never_allowed_str, $str);
+
+		foreach ($this->_never_allowed_regex as $regex)
+		{
+			$str = preg_replace('#'.$regex.'#is', '[removed]', $str);
+		}
+
+		return $str;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set CSRF Hash and Cookie
+	 *
+	 * @return	string
+	 */
+	protected function _csrf_set_hash()
+	{
+		if ($this->_csrf_hash === NULL)
+		{
+			// If the cookie exists we will use its value.
+			// We don't necessarily want to regenerate it with
+			// each page load since a page could contain embedded
+			// sub-pages causing this feature to fail
+			if (isset($_COOKIE[$this->_csrf_cookie_name]) && is_string($_COOKIE[$this->_csrf_cookie_name])
+				&& preg_match('#^[0-9a-f]{32}$#iS', $_COOKIE[$this->_csrf_cookie_name]) === 1)
+			{
+				return $this->_csrf_hash = $_COOKIE[$this->_csrf_cookie_name];
+			}
+
+			$rand = $this->get_random_bytes(16);
+			$this->_csrf_hash = ($rand === FALSE)
+				? md5(uniqid(mt_rand(), TRUE))
+				: bin2hex($rand);
+		}
+
+		return $this->_csrf_hash;
+	}
+
+}
diff --git a/system/core/URI.php b/system/core/URI.php
new file mode 100644
index 0000000..6a55439
--- /dev/null
+++ b/system/core/URI.php
@@ -0,0 +1,644 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * URI Class
+ *
+ * Parses URIs and determines routing
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	URI
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/libraries/uri.html
+ */
+class CI_URI {
+
+	/**
+	 * List of cached URI segments
+	 *
+	 * @var	array
+	 */
+	public $keyval = array();
+
+	/**
+	 * Current URI string
+	 *
+	 * @var	string
+	 */
+	public $uri_string = '';
+
+	/**
+	 * List of URI segments
+	 *
+	 * Starts at 1 instead of 0.
+	 *
+	 * @var	array
+	 */
+	public $segments = array();
+
+	/**
+	 * List of routed URI segments
+	 *
+	 * Starts at 1 instead of 0.
+	 *
+	 * @var	array
+	 */
+	public $rsegments = array();
+
+	/**
+	 * Permitted URI chars
+	 *
+	 * PCRE character group allowed in URI segments
+	 *
+	 * @var	string
+	 */
+	protected $_permitted_uri_chars;
+
+	/**
+	 * Class constructor
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		$this->config =& load_class('Config', 'core');
+
+		// If query strings are enabled, we don't need to parse any segments.
+		// However, they don't make sense under CLI.
+		if (is_cli() OR $this->config->item('enable_query_strings') !== TRUE)
+		{
+			$this->_permitted_uri_chars = $this->config->item('permitted_uri_chars');
+
+			// If it's a CLI request, ignore the configuration
+			if (is_cli())
+			{
+				$uri = $this->_parse_argv();
+			}
+			else
+			{
+				$protocol = $this->config->item('uri_protocol');
+				empty($protocol) && $protocol = 'REQUEST_URI';
+
+				switch ($protocol)
+				{
+					case 'AUTO': // For BC purposes only
+					case 'REQUEST_URI':
+						$uri = $this->_parse_request_uri();
+						break;
+					case 'QUERY_STRING':
+						$uri = $this->_parse_query_string();
+						break;
+					case 'PATH_INFO':
+					default:
+						$uri = isset($_SERVER[$protocol])
+							? $_SERVER[$protocol]
+							: $this->_parse_request_uri();
+						break;
+				}
+			}
+
+			$this->_set_uri_string($uri);
+		}
+
+		log_message('info', 'URI Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set URI String
+	 *
+	 * @param 	string	$str
+	 * @return	void
+	 */
+	protected function _set_uri_string($str)
+	{
+		// Filter out control characters and trim slashes
+		$this->uri_string = trim(remove_invisible_characters($str, FALSE), '/');
+
+		if ($this->uri_string !== '')
+		{
+			// Remove the URL suffix, if present
+			if (($suffix = (string) $this->config->item('url_suffix')) !== '')
+			{
+				$slen = strlen($suffix);
+
+				if (substr($this->uri_string, -$slen) === $suffix)
+				{
+					$this->uri_string = substr($this->uri_string, 0, -$slen);
+				}
+			}
+
+			$this->segments[0] = NULL;
+			// Populate the segments array
+			foreach (explode('/', trim($this->uri_string, '/')) as $val)
+			{
+				$val = trim($val);
+				// Filter segments for security
+				$this->filter_uri($val);
+
+				if ($val !== '')
+				{
+					$this->segments[] = $val;
+				}
+			}
+
+			unset($this->segments[0]);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse REQUEST_URI
+	 *
+	 * Will parse REQUEST_URI and automatically detect the URI from it,
+	 * while fixing the query string if necessary.
+	 *
+	 * @return	string
+	 */
+	protected function _parse_request_uri()
+	{
+		if ( ! isset($_SERVER['REQUEST_URI'], $_SERVER['SCRIPT_NAME']))
+		{
+			return '';
+		}
+
+		// parse_url() returns false if no host is present, but the path or query string
+		// contains a colon followed by a number
+		$uri = parse_url('http://dummy'.$_SERVER['REQUEST_URI']);
+		$query = isset($uri['query']) ? $uri['query'] : '';
+		$uri = isset($uri['path']) ? $uri['path'] : '';
+
+		if (isset($_SERVER['SCRIPT_NAME'][0]))
+		{
+			if (strpos($uri, $_SERVER['SCRIPT_NAME']) === 0)
+			{
+				$uri = (string) substr($uri, strlen($_SERVER['SCRIPT_NAME']));
+			}
+			elseif (strpos($uri, dirname($_SERVER['SCRIPT_NAME'])) === 0)
+			{
+				$uri = (string) substr($uri, strlen(dirname($_SERVER['SCRIPT_NAME'])));
+			}
+		}
+
+		// This section ensures that even on servers that require the URI to be in the query string (Nginx) a correct
+		// URI is found, and also fixes the QUERY_STRING server var and $_GET array.
+		if (trim($uri, '/') === '' && strncmp($query, '/', 1) === 0)
+		{
+			$query = explode('?', $query, 2);
+			$uri = $query[0];
+			$_SERVER['QUERY_STRING'] = isset($query[1]) ? $query[1] : '';
+		}
+		else
+		{
+			$_SERVER['QUERY_STRING'] = $query;
+		}
+
+		parse_str($_SERVER['QUERY_STRING'], $_GET);
+
+		if ($uri === '/' OR $uri === '')
+		{
+			return '/';
+		}
+
+		// Do some final cleaning of the URI and return it
+		return $this->_remove_relative_directory($uri);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse QUERY_STRING
+	 *
+	 * Will parse QUERY_STRING and automatically detect the URI from it.
+	 *
+	 * @return	string
+	 */
+	protected function _parse_query_string()
+	{
+		$uri = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : @getenv('QUERY_STRING');
+
+		if (trim($uri, '/') === '')
+		{
+			return '';
+		}
+		elseif (strncmp($uri, '/', 1) === 0)
+		{
+			$uri = explode('?', $uri, 2);
+			$_SERVER['QUERY_STRING'] = isset($uri[1]) ? $uri[1] : '';
+			$uri = $uri[0];
+		}
+
+		parse_str($_SERVER['QUERY_STRING'], $_GET);
+
+		return $this->_remove_relative_directory($uri);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Parse CLI arguments
+	 *
+	 * Take each command line argument and assume it is a URI segment.
+	 *
+	 * @return	string
+	 */
+	protected function _parse_argv()
+	{
+		$args = array_slice($_SERVER['argv'], 1);
+		return $args ? implode('/', $args) : '';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Remove relative directory (../) and multi slashes (///)
+	 *
+	 * Do some final cleaning of the URI and return it, currently only used in self::_parse_request_uri()
+	 *
+	 * @param	string	$uri
+	 * @return	string
+	 */
+	protected function _remove_relative_directory($uri)
+	{
+		$uris = array();
+		$tok = strtok($uri, '/');
+		while ($tok !== FALSE)
+		{
+			if (( ! empty($tok) OR $tok === '0') && $tok !== '..')
+			{
+				$uris[] = $tok;
+			}
+			$tok = strtok('/');
+		}
+
+		return implode('/', $uris);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Filter URI
+	 *
+	 * Filters segments for malicious characters.
+	 *
+	 * @param	string	$str
+	 * @return	void
+	 */
+	public function filter_uri(&$str)
+	{
+		if ( ! empty($str) && ! empty($this->_permitted_uri_chars) && ! preg_match('/^['.$this->_permitted_uri_chars.']+$/i'.(UTF8_ENABLED ? 'u' : ''), $str))
+		{
+			show_error('The URI you submitted has disallowed characters.', 400);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch URI Segment
+	 *
+	 * @see		CI_URI::$segments
+	 * @param	int		$n		Index
+	 * @param	mixed		$no_result	What to return if the segment index is not found
+	 * @return	mixed
+	 */
+	public function segment($n, $no_result = NULL)
+	{
+		return isset($this->segments[$n]) ? $this->segments[$n] : $no_result;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch URI "routed" Segment
+	 *
+	 * Returns the re-routed URI segment (assuming routing rules are used)
+	 * based on the index provided. If there is no routing, will return
+	 * the same result as CI_URI::segment().
+	 *
+	 * @see		CI_URI::$rsegments
+	 * @see		CI_URI::segment()
+	 * @param	int		$n		Index
+	 * @param	mixed		$no_result	What to return if the segment index is not found
+	 * @return	mixed
+	 */
+	public function rsegment($n, $no_result = NULL)
+	{
+		return isset($this->rsegments[$n]) ? $this->rsegments[$n] : $no_result;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * URI to assoc
+	 *
+	 * Generates an associative array of URI data starting at the supplied
+	 * segment index. For example, if this is your URI:
+	 *
+	 *	example.com/user/search/name/joe/location/UK/gender/male
+	 *
+	 * You can use this method to generate an array with this prototype:
+	 *
+	 *	array (
+	 *		name => joe
+	 *		location => UK
+	 *		gender => male
+	 *	 )
+	 *
+	 * @param	int	$n		Index (default: 3)
+	 * @param	array	$default	Default values
+	 * @return	array
+	 */
+	public function uri_to_assoc($n = 3, $default = array())
+	{
+		return $this->_uri_to_assoc($n, $default, 'segment');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Routed URI to assoc
+	 *
+	 * Identical to CI_URI::uri_to_assoc(), only it uses the re-routed
+	 * segment array.
+	 *
+	 * @see		CI_URI::uri_to_assoc()
+	 * @param 	int	$n		Index (default: 3)
+	 * @param 	array	$default	Default values
+	 * @return 	array
+	 */
+	public function ruri_to_assoc($n = 3, $default = array())
+	{
+		return $this->_uri_to_assoc($n, $default, 'rsegment');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Internal URI-to-assoc
+	 *
+	 * Generates a key/value pair from the URI string or re-routed URI string.
+	 *
+	 * @used-by	CI_URI::uri_to_assoc()
+	 * @used-by	CI_URI::ruri_to_assoc()
+	 * @param	int	$n		Index (default: 3)
+	 * @param	array	$default	Default values
+	 * @param	string	$which		Array name ('segment' or 'rsegment')
+	 * @return	array
+	 */
+	protected function _uri_to_assoc($n = 3, $default = array(), $which = 'segment')
+	{
+		if ( ! is_numeric($n))
+		{
+			return $default;
+		}
+
+		if (isset($this->keyval[$which], $this->keyval[$which][$n]))
+		{
+			return $this->keyval[$which][$n];
+		}
+
+		$total_segments = "total_{$which}s";
+		$segment_array = "{$which}_array";
+
+		if ($this->$total_segments() < $n)
+		{
+			return (count($default) === 0)
+				? array()
+				: array_fill_keys($default, NULL);
+		}
+
+		$segments = array_slice($this->$segment_array(), ($n - 1));
+		$i = 0;
+		$lastval = '';
+		$retval = array();
+		foreach ($segments as $seg)
+		{
+			if ($i % 2)
+			{
+				$retval[$lastval] = $seg;
+			}
+			else
+			{
+				$retval[$seg] = NULL;
+				$lastval = $seg;
+			}
+
+			$i++;
+		}
+
+		if (count($default) > 0)
+		{
+			foreach ($default as $val)
+			{
+				if ( ! array_key_exists($val, $retval))
+				{
+					$retval[$val] = NULL;
+				}
+			}
+		}
+
+		// Cache the array for reuse
+		isset($this->keyval[$which]) OR $this->keyval[$which] = array();
+		$this->keyval[$which][$n] = $retval;
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Assoc to URI
+	 *
+	 * Generates a URI string from an associative array.
+	 *
+	 * @param	array	$array	Input array of key/value pairs
+	 * @return	string	URI string
+	 */
+	public function assoc_to_uri($array)
+	{
+		$temp = array();
+		foreach ((array) $array as $key => $val)
+		{
+			$temp[] = $key;
+			$temp[] = $val;
+		}
+
+		return implode('/', $temp);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Slash segment
+	 *
+	 * Fetches an URI segment with a slash.
+	 *
+	 * @param	int	$n	Index
+	 * @param	string	$where	Where to add the slash ('trailing' or 'leading')
+	 * @return	string
+	 */
+	public function slash_segment($n, $where = 'trailing')
+	{
+		return $this->_slash_segment($n, $where, 'segment');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Slash routed segment
+	 *
+	 * Fetches an URI routed segment with a slash.
+	 *
+	 * @param	int	$n	Index
+	 * @param	string	$where	Where to add the slash ('trailing' or 'leading')
+	 * @return	string
+	 */
+	public function slash_rsegment($n, $where = 'trailing')
+	{
+		return $this->_slash_segment($n, $where, 'rsegment');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Internal Slash segment
+	 *
+	 * Fetches an URI Segment and adds a slash to it.
+	 *
+	 * @used-by	CI_URI::slash_segment()
+	 * @used-by	CI_URI::slash_rsegment()
+	 *
+	 * @param	int	$n	Index
+	 * @param	string	$where	Where to add the slash ('trailing' or 'leading')
+	 * @param	string	$which	Array name ('segment' or 'rsegment')
+	 * @return	string
+	 */
+	protected function _slash_segment($n, $where = 'trailing', $which = 'segment')
+	{
+		$leading = $trailing = '/';
+
+		if ($where === 'trailing')
+		{
+			$leading	= '';
+		}
+		elseif ($where === 'leading')
+		{
+			$trailing	= '';
+		}
+
+		return $leading.$this->$which($n).$trailing;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Segment Array
+	 *
+	 * @return	array	CI_URI::$segments
+	 */
+	public function segment_array()
+	{
+		return $this->segments;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Routed Segment Array
+	 *
+	 * @return	array	CI_URI::$rsegments
+	 */
+	public function rsegment_array()
+	{
+		return $this->rsegments;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Total number of segments
+	 *
+	 * @return	int
+	 */
+	public function total_segments()
+	{
+		return count($this->segments);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Total number of routed segments
+	 *
+	 * @return	int
+	 */
+	public function total_rsegments()
+	{
+		return count($this->rsegments);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch URI string
+	 *
+	 * @return	string	CI_URI::$uri_string
+	 */
+	public function uri_string()
+	{
+		return $this->uri_string;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Re-routed URI string
+	 *
+	 * @return	string
+	 */
+	public function ruri_string()
+	{
+		return ltrim(load_class('Router', 'core')->directory, '/').implode('/', $this->rsegments);
+	}
+
+}
diff --git a/system/core/Utf8.php b/system/core/Utf8.php
new file mode 100644
index 0000000..0547223
--- /dev/null
+++ b/system/core/Utf8.php
@@ -0,0 +1,165 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 2.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Utf8 Class
+ *
+ * Provides support for UTF-8 environments
+ *
+ * @package		CodeIgniter
+ * @subpackage	Libraries
+ * @category	UTF-8
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/libraries/utf8.html
+ */
+class CI_Utf8 {
+
+	/**
+	 * Class constructor
+	 *
+	 * Determines if UTF-8 support is to be enabled.
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		if (
+			defined('PREG_BAD_UTF8_ERROR')				// PCRE must support UTF-8
+			&& (ICONV_ENABLED === TRUE OR MB_ENABLED === TRUE)	// iconv or mbstring must be installed
+			&& strtoupper(config_item('charset')) === 'UTF-8'	// Application charset must be UTF-8
+			)
+		{
+			define('UTF8_ENABLED', TRUE);
+			log_message('debug', 'UTF-8 Support Enabled');
+		}
+		else
+		{
+			define('UTF8_ENABLED', FALSE);
+			log_message('debug', 'UTF-8 Support Disabled');
+		}
+
+		log_message('info', 'Utf8 Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Clean UTF-8 strings
+	 *
+	 * Ensures strings contain only valid UTF-8 characters.
+	 *
+	 * @param	string	$str	String to clean
+	 * @return	string
+	 */
+	public function clean_string($str)
+	{
+		if ($this->is_ascii($str) === FALSE)
+		{
+			if (MB_ENABLED)
+			{
+				$str = mb_convert_encoding($str, 'UTF-8', 'UTF-8');
+			}
+			elseif (ICONV_ENABLED)
+			{
+				$str = @iconv('UTF-8', 'UTF-8//IGNORE', $str);
+			}
+		}
+
+		return $str;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Remove ASCII control characters
+	 *
+	 * Removes all ASCII control characters except horizontal tabs,
+	 * line feeds, and carriage returns, as all others can cause
+	 * problems in XML.
+	 *
+	 * @param	string	$str	String to clean
+	 * @return	string
+	 */
+	public function safe_ascii_for_xml($str)
+	{
+		return remove_invisible_characters($str, FALSE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Convert to UTF-8
+	 *
+	 * Attempts to convert a string to UTF-8.
+	 *
+	 * @param	string	$str		Input string
+	 * @param	string	$encoding	Input encoding
+	 * @return	string	$str encoded in UTF-8 or FALSE on failure
+	 */
+	public function convert_to_utf8($str, $encoding)
+	{
+		if (MB_ENABLED)
+		{
+			return mb_convert_encoding($str, 'UTF-8', $encoding);
+		}
+		elseif (ICONV_ENABLED)
+		{
+			return @iconv($encoding, 'UTF-8', $str);
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Is ASCII?
+	 *
+	 * Tests if a string is standard 7-bit ASCII or not.
+	 *
+	 * @param	string	$str	String to check
+	 * @return	bool
+	 */
+	public function is_ascii($str)
+	{
+		return (preg_match('/[^\x00-\x7F]/S', $str) === 0);
+	}
+
+}
diff --git a/system/core/compat/hash.php b/system/core/compat/hash.php
new file mode 100644
index 0000000..3fe3b85
--- /dev/null
+++ b/system/core/compat/hash.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) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PHP ext/hash compatibility package
+ *
+ * @package		CodeIgniter
+ * @subpackage	CodeIgniter
+ * @category	Compatibility
+ * @author		Andrey Andreev
+ * @link		https://codeigniter.com/userguide3/
+ * @link		https://secure.php.net/hash
+ */
+
+// ------------------------------------------------------------------------
+
+if (is_php('5.6'))
+{
+	return;
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('hash_equals'))
+{
+	/**
+	 * hash_equals()
+	 *
+	 * @link	http://php.net/hash_equals
+	 * @param	string	$known_string
+	 * @param	string	$user_string
+	 * @return	bool
+	 */
+	function hash_equals($known_string, $user_string)
+	{
+		if ( ! is_string($known_string))
+		{
+			trigger_error('hash_equals(): Expected known_string to be a string, '.strtolower(gettype($known_string)).' given', E_USER_WARNING);
+			return FALSE;
+		}
+		elseif ( ! is_string($user_string))
+		{
+			trigger_error('hash_equals(): Expected user_string to be a string, '.strtolower(gettype($user_string)).' given', E_USER_WARNING);
+			return FALSE;
+		}
+		elseif (($length = strlen($known_string)) !== strlen($user_string))
+		{
+			return FALSE;
+		}
+
+		$diff = 0;
+		for ($i = 0; $i < $length; $i++)
+		{
+			$diff |= ord($known_string[$i]) ^ ord($user_string[$i]);
+		}
+
+		return ($diff === 0);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if (is_php('5.5'))
+{
+	return;
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('hash_pbkdf2'))
+{
+	/**
+	 * hash_pbkdf2()
+	 *
+	 * @link	http://php.net/hash_pbkdf2
+	 * @param	string	$algo
+	 * @param	string	$password
+	 * @param	string	$salt
+	 * @param	int	$iterations
+	 * @param	int	$length
+	 * @param	bool	$raw_output
+	 * @return	string
+	 */
+	function hash_pbkdf2($algo, $password, $salt, $iterations, $length = 0, $raw_output = FALSE)
+	{
+		if ( ! in_array(strtolower($algo), hash_algos(), TRUE))
+		{
+			trigger_error('hash_pbkdf2(): Unknown hashing algorithm: '.$algo, E_USER_WARNING);
+			return FALSE;
+		}
+
+		if (($type = gettype($iterations)) !== 'integer')
+		{
+			if ($type === 'object' && method_exists($iterations, '__toString'))
+			{
+				$iterations = (string) $iterations;
+			}
+
+			if (is_string($iterations) && is_numeric($iterations))
+			{
+				$iterations = (int) $iterations;
+			}
+			else
+			{
+				trigger_error('hash_pbkdf2() expects parameter 4 to be long, '.$type.' given', E_USER_WARNING);
+				return NULL;
+			}
+		}
+
+		if ($iterations < 1)
+		{
+			trigger_error('hash_pbkdf2(): Iterations must be a positive integer: '.$iterations, E_USER_WARNING);
+			return FALSE;
+		}
+
+		if (($type = gettype($length)) !== 'integer')
+		{
+			if ($type === 'object' && method_exists($length, '__toString'))
+			{
+				$length = (string) $length;
+			}
+
+			if (is_string($length) && is_numeric($length))
+			{
+				$length = (int) $length;
+			}
+			else
+			{
+				trigger_error('hash_pbkdf2() expects parameter 5 to be long, '.$type.' given', E_USER_WARNING);
+				return NULL;
+			}
+		}
+
+		if ($length < 0)
+		{
+			trigger_error('hash_pbkdf2(): Length must be greater than or equal to 0: '.$length, E_USER_WARNING);
+			return FALSE;
+		}
+
+		$hash_length = defined('MB_OVERLOAD_STRING')
+			? mb_strlen(hash($algo, NULL, TRUE), '8bit')
+			: strlen(hash($algo, NULL, TRUE));
+		empty($length) && $length = $hash_length;
+
+		// Pre-hash password inputs longer than the algorithm's block size
+		// (i.e. prepare HMAC key) to mitigate potential DoS attacks.
+		static $block_sizes;
+		empty($block_sizes) && $block_sizes = array(
+			'gost' => 32,
+			'haval128,3' => 128,
+			'haval160,3' => 128,
+			'haval192,3' => 128,
+			'haval224,3' => 128,
+			'haval256,3' => 128,
+			'haval128,4' => 128,
+			'haval160,4' => 128,
+			'haval192,4' => 128,
+			'haval224,4' => 128,
+			'haval256,4' => 128,
+			'haval128,5' => 128,
+			'haval160,5' => 128,
+			'haval192,5' => 128,
+			'haval224,5' => 128,
+			'haval256,5' => 128,
+			'md2' => 16,
+			'md4' => 64,
+			'md5' => 64,
+			'ripemd128' => 64,
+			'ripemd160' => 64,
+			'ripemd256' => 64,
+			'ripemd320' => 64,
+			'salsa10' => 64,
+			'salsa20' => 64,
+			'sha1' => 64,
+			'sha224' => 64,
+			'sha256' => 64,
+			'sha384' => 128,
+			'sha512' => 128,
+			'snefru' => 32,
+			'snefru256' => 32,
+			'tiger128,3' => 64,
+			'tiger160,3' => 64,
+			'tiger192,3' => 64,
+			'tiger128,4' => 64,
+			'tiger160,4' => 64,
+			'tiger192,4' => 64,
+			'whirlpool' => 64
+		);
+
+		if (isset($block_sizes[$algo], $password[$block_sizes[$algo]]))
+		{
+			$password = hash($algo, $password, TRUE);
+		}
+
+		$hash = '';
+		// Note: Blocks are NOT 0-indexed
+		for ($bc = (int) ceil($length / $hash_length), $bi = 1; $bi <= $bc; $bi++)
+		{
+			$key = $derived_key = hash_hmac($algo, $salt.pack('N', $bi), $password, TRUE);
+			for ($i = 1; $i < $iterations; $i++)
+			{
+				$derived_key ^= $key = hash_hmac($algo, $key, $password, TRUE);
+			}
+
+			$hash .= $derived_key;
+		}
+
+		// This is not RFC-compatible, but we're aiming for natural PHP compatibility
+		if ( ! $raw_output)
+		{
+			$hash = bin2hex($hash);
+		}
+
+		return defined('MB_OVERLOAD_STRING')
+			? mb_substr($hash, 0, $length, '8bit')
+			: substr($hash, 0, $length);
+	}
+}
diff --git a/system/core/compat/index.html b/system/core/compat/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/core/compat/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/system/core/compat/mbstring.php b/system/core/compat/mbstring.php
new file mode 100644
index 0000000..1c49d18
--- /dev/null
+++ b/system/core/compat/mbstring.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PHP ext/mbstring compatibility package
+ *
+ * @package		CodeIgniter
+ * @subpackage	CodeIgniter
+ * @category	Compatibility
+ * @author		Andrey Andreev
+ * @link		https://codeigniter.com/userguide3/
+ * @link		https://secure.php.net/mbstring
+ */
+
+// ------------------------------------------------------------------------
+
+if (MB_ENABLED === TRUE)
+{
+	return;
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('mb_strlen'))
+{
+	/**
+	 * mb_strlen()
+	 *
+	 * WARNING: This function WILL fall-back to strlen()
+	 * if iconv is not available!
+	 *
+	 * @link	http://php.net/mb_strlen
+	 * @param	string	$str
+	 * @param	string	$encoding
+	 * @return	int
+	 */
+	function mb_strlen($str, $encoding = NULL)
+	{
+		if (ICONV_ENABLED === TRUE)
+		{
+			return iconv_strlen($str, isset($encoding) ? $encoding : config_item('charset'));
+		}
+
+		log_message('debug', 'Compatibility (mbstring): iconv_strlen() is not available, falling back to strlen().');
+		return strlen($str);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('mb_strpos'))
+{
+	/**
+	 * mb_strpos()
+	 *
+	 * WARNING: This function WILL fall-back to strpos()
+	 * if iconv is not available!
+	 *
+	 * @link	http://php.net/mb_strpos
+	 * @param	string	$haystack
+	 * @param	string	$needle
+	 * @param	int	$offset
+	 * @param	string	$encoding
+	 * @return	mixed
+	 */
+	function mb_strpos($haystack, $needle, $offset = 0, $encoding = NULL)
+	{
+		if (ICONV_ENABLED === TRUE)
+		{
+			return iconv_strpos($haystack, $needle, $offset, isset($encoding) ? $encoding : config_item('charset'));
+		}
+
+		log_message('debug', 'Compatibility (mbstring): iconv_strpos() is not available, falling back to strpos().');
+		return strpos($haystack, $needle, $offset);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('mb_substr'))
+{
+	/**
+	 * mb_substr()
+	 *
+	 * WARNING: This function WILL fall-back to substr()
+	 * if iconv is not available.
+	 *
+	 * @link	http://php.net/mb_substr
+	 * @param	string	$str
+	 * @param	int	$start
+	 * @param	int 	$length
+	 * @param	string	$encoding
+	 * @return	string
+	 */
+	function mb_substr($str, $start, $length = NULL, $encoding = NULL)
+	{
+		if (ICONV_ENABLED === TRUE)
+		{
+			isset($encoding) OR $encoding = config_item('charset');
+			return iconv_substr(
+				$str,
+				$start,
+				isset($length) ? $length : iconv_strlen($str, $encoding), // NULL doesn't work
+				$encoding
+			);
+		}
+
+		log_message('debug', 'Compatibility (mbstring): iconv_substr() is not available, falling back to substr().');
+		return isset($length)
+			? substr($str, $start, $length)
+			: substr($str, $start);
+	}
+}
diff --git a/system/core/compat/password.php b/system/core/compat/password.php
new file mode 100644
index 0000000..9937a47
--- /dev/null
+++ b/system/core/compat/password.php
@@ -0,0 +1,252 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PHP ext/standard/password compatibility package
+ *
+ * @package		CodeIgniter
+ * @subpackage	CodeIgniter
+ * @category	Compatibility
+ * @author		Andrey Andreev
+ * @link		https://codeigniter.com/userguide3/
+ * @link		https://secure.php.net/password
+ */
+
+// ------------------------------------------------------------------------
+
+if (is_php('5.5') OR ! defined('CRYPT_BLOWFISH') OR CRYPT_BLOWFISH !== 1 OR defined('HHVM_VERSION'))
+{
+	return;
+}
+
+// ------------------------------------------------------------------------
+
+defined('PASSWORD_BCRYPT') OR define('PASSWORD_BCRYPT', 1);
+defined('PASSWORD_DEFAULT') OR define('PASSWORD_DEFAULT', PASSWORD_BCRYPT);
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('password_get_info'))
+{
+	/**
+	 * password_get_info()
+	 *
+	 * @link	http://php.net/password_get_info
+	 * @param	string	$hash
+	 * @return	array
+	 */
+	function password_get_info($hash)
+	{
+		return (strlen($hash) < 60 OR sscanf($hash, '$2y$%d', $hash) !== 1)
+			? array('algo' => 0, 'algoName' => 'unknown', 'options' => array())
+			: array('algo' => 1, 'algoName' => 'bcrypt', 'options' => array('cost' => $hash));
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('password_hash'))
+{
+	/**
+	 * password_hash()
+	 *
+	 * @link	http://php.net/password_hash
+	 * @param	string	$password
+	 * @param	int	$algo
+	 * @param	array	$options
+	 * @return	mixed
+	 */
+	function password_hash($password, $algo, array $options = array())
+	{
+		static $func_overload;
+		isset($func_overload) OR $func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload'));
+
+		if ($algo !== 1)
+		{
+			trigger_error('password_hash(): Unknown hashing algorithm: '.(int) $algo, E_USER_WARNING);
+			return NULL;
+		}
+
+		if (isset($options['cost']) && ($options['cost'] < 4 OR $options['cost'] > 31))
+		{
+			trigger_error('password_hash(): Invalid bcrypt cost parameter specified: '.(int) $options['cost'], E_USER_WARNING);
+			return NULL;
+		}
+
+		if (isset($options['salt']) && ($saltlen = ($func_overload ? mb_strlen($options['salt'], '8bit') : strlen($options['salt']))) < 22)
+		{
+			trigger_error('password_hash(): Provided salt is too short: '.$saltlen.' expecting 22', E_USER_WARNING);
+			return NULL;
+		}
+		elseif ( ! isset($options['salt']))
+		{
+			if (function_exists('random_bytes'))
+			{
+				try
+				{
+					$options['salt'] = random_bytes(16);
+				}
+				catch (Exception $e)
+				{
+					log_message('error', 'compat/password: Error while trying to use random_bytes(): '.$e->getMessage());
+					return FALSE;
+				}
+			}
+			elseif (defined('MCRYPT_DEV_URANDOM'))
+			{
+				$options['salt'] = mcrypt_create_iv(16, MCRYPT_DEV_URANDOM);
+			}
+			elseif (DIRECTORY_SEPARATOR === '/' && (is_readable($dev = '/dev/arandom') OR is_readable($dev = '/dev/urandom')))
+			{
+				if (($fp = fopen($dev, 'rb')) === FALSE)
+				{
+					log_message('error', 'compat/password: Unable to open '.$dev.' for reading.');
+					return FALSE;
+				}
+
+				// Try not to waste entropy ...
+				is_php('5.4') && stream_set_chunk_size($fp, 16);
+
+				$options['salt'] = '';
+				for ($read = 0; $read < 16; $read = ($func_overload) ? mb_strlen($options['salt'], '8bit') : strlen($options['salt']))
+				{
+					if (($read = fread($fp, 16 - $read)) === FALSE)
+					{
+						log_message('error', 'compat/password: Error while reading from '.$dev.'.');
+						return FALSE;
+					}
+					$options['salt'] .= $read;
+				}
+
+				fclose($fp);
+			}
+			elseif (function_exists('openssl_random_pseudo_bytes'))
+			{
+				$is_secure = NULL;
+				$options['salt'] = openssl_random_pseudo_bytes(16, $is_secure);
+				if ($is_secure !== TRUE)
+				{
+					log_message('error', 'compat/password: openssl_random_pseudo_bytes() set the $cryto_strong flag to FALSE');
+					return FALSE;
+				}
+			}
+			else
+			{
+				log_message('error', 'compat/password: No CSPRNG available.');
+				return FALSE;
+			}
+
+			$options['salt'] = str_replace('+', '.', rtrim(base64_encode($options['salt']), '='));
+		}
+		elseif ( ! preg_match('#^[a-zA-Z0-9./]+$#D', $options['salt']))
+		{
+			$options['salt'] = str_replace('+', '.', rtrim(base64_encode($options['salt']), '='));
+		}
+
+		isset($options['cost']) OR $options['cost'] = 10;
+
+		return (strlen($password = crypt($password, sprintf('$2y$%02d$%s', $options['cost'], $options['salt']))) === 60)
+			? $password
+			: FALSE;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('password_needs_rehash'))
+{
+	/**
+	 * password_needs_rehash()
+	 *
+	 * @link	http://php.net/password_needs_rehash
+	 * @param	string	$hash
+	 * @param	int	$algo
+	 * @param	array	$options
+	 * @return	bool
+	 */
+	function password_needs_rehash($hash, $algo, array $options = array())
+	{
+		$info = password_get_info($hash);
+
+		if ($algo !== $info['algo'])
+		{
+			return TRUE;
+		}
+		elseif ($algo === 1)
+		{
+			$options['cost'] = isset($options['cost']) ? (int) $options['cost'] : 10;
+			return ($info['options']['cost'] !== $options['cost']);
+		}
+
+		// Odd at first glance, but according to a comment in PHP's own unit tests,
+		// because it is an unknown algorithm - it's valid and therefore doesn't
+		// need rehashing.
+		return FALSE;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('password_verify'))
+{
+	/**
+	 * password_verify()
+	 *
+	 * @link	http://php.net/password_verify
+	 * @param	string	$password
+	 * @param	string	$hash
+	 * @return	bool
+	 */
+	function password_verify($password, $hash)
+	{
+		if (strlen($hash) !== 60 OR strlen($password = crypt($password, $hash)) !== 60)
+		{
+			return FALSE;
+		}
+
+		$compare = 0;
+		for ($i = 0; $i < 60; $i++)
+		{
+			$compare |= (ord($password[$i]) ^ ord($hash[$i]));
+		}
+
+		return ($compare === 0);
+	}
+}
diff --git a/system/core/compat/standard.php b/system/core/compat/standard.php
new file mode 100644
index 0000000..18b1281
--- /dev/null
+++ b/system/core/compat/standard.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PHP ext/standard compatibility package
+ *
+ * @package		CodeIgniter
+ * @subpackage	CodeIgniter
+ * @category	Compatibility
+ * @author		Andrey Andreev
+ * @link		https://codeigniter.com/userguide3/
+ */
+
+// ------------------------------------------------------------------------
+
+if (is_php('5.5'))
+{
+	return;
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('array_column'))
+{
+	/**
+	 * array_column()
+	 *
+	 * @link	http://php.net/array_column
+	 * @param	array	$array
+	 * @param	mixed	$column_key
+	 * @param	mixed	$index_key
+	 * @return	array
+	 */
+	function array_column(array $array, $column_key, $index_key = NULL)
+	{
+		if ( ! in_array($type = gettype($column_key), array('integer', 'string', 'NULL'), TRUE))
+		{
+			if ($type === 'double')
+			{
+				$column_key = (int) $column_key;
+			}
+			elseif ($type === 'object' && method_exists($column_key, '__toString'))
+			{
+				$column_key = (string) $column_key;
+			}
+			else
+			{
+				trigger_error('array_column(): The column key should be either a string or an integer', E_USER_WARNING);
+				return FALSE;
+			}
+		}
+
+		if ( ! in_array($type = gettype($index_key), array('integer', 'string', 'NULL'), TRUE))
+		{
+			if ($type === 'double')
+			{
+				$index_key = (int) $index_key;
+			}
+			elseif ($type === 'object' && method_exists($index_key, '__toString'))
+			{
+				$index_key = (string) $index_key;
+			}
+			else
+			{
+				trigger_error('array_column(): The index key should be either a string or an integer', E_USER_WARNING);
+				return FALSE;
+			}
+		}
+
+		$result = array();
+		foreach ($array as &$a)
+		{
+			if ($column_key === NULL)
+			{
+				$value = $a;
+			}
+			elseif (is_array($a) && array_key_exists($column_key, $a))
+			{
+				$value = $a[$column_key];
+			}
+			else
+			{
+				continue;
+			}
+
+			if ($index_key === NULL OR ! array_key_exists($index_key, $a))
+			{
+				$result[] = $value;
+			}
+			else
+			{
+				$result[$a[$index_key]] = $value;
+			}
+		}
+
+		return $result;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if (is_php('5.4'))
+{
+	return;
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('hex2bin'))
+{
+	/**
+	 * hex2bin()
+	 *
+	 * @link	http://php.net/hex2bin
+	 * @param	string	$data
+	 * @return	string
+	 */
+	function hex2bin($data)
+	{
+		if (in_array($type = gettype($data), array('array', 'double', 'object', 'resource'), TRUE))
+		{
+			if ($type === 'object' && method_exists($data, '__toString'))
+			{
+				$data = (string) $data;
+			}
+			else
+			{
+				trigger_error('hex2bin() expects parameter 1 to be string, '.$type.' given', E_USER_WARNING);
+				return NULL;
+			}
+		}
+
+		if (strlen($data) % 2 !== 0)
+		{
+			trigger_error('Hexadecimal input string must have an even length', E_USER_WARNING);
+			return FALSE;
+		}
+		elseif ( ! preg_match('/^[0-9a-f]*$/i', $data))
+		{
+			trigger_error('Input string must be hexadecimal string', E_USER_WARNING);
+			return FALSE;
+		}
+
+		return pack('H*', $data);
+	}
+}
diff --git a/system/core/index.html b/system/core/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/core/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/system/database/DB.php b/system/database/DB.php
new file mode 100644
index 0000000..23581af
--- /dev/null
+++ b/system/database/DB.php
@@ -0,0 +1,219 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Initialize the database
+ *
+ * @category	Database
+ * @author	EllisLab Dev Team
+ * @link	https://codeigniter.com/userguide3/database/
+ *
+ * @param 	string|string[]	$params
+ * @param 	bool		$query_builder_override
+ *				Determines if query builder should be used or not
+ */
+function &DB($params = '', $query_builder_override = NULL)
+{
+	// Load the DB config file if a DSN string wasn't passed
+	if (is_string($params) && strpos($params, '://') === FALSE)
+	{
+		// Is the config file in the environment folder?
+		if ( ! file_exists($file_path = APPPATH.'config/'.ENVIRONMENT.'/database.php')
+			&& ! file_exists($file_path = APPPATH.'config/database.php'))
+		{
+			show_error('The configuration file database.php does not exist.');
+		}
+
+		include($file_path);
+
+		// Make packages contain database config files,
+		// given that the controller instance already exists
+		if (class_exists('CI_Controller', FALSE))
+		{
+			foreach (get_instance()->load->get_package_paths() as $path)
+			{
+				if ($path !== APPPATH)
+				{
+					if (file_exists($file_path = $path.'config/'.ENVIRONMENT.'/database.php'))
+					{
+						include($file_path);
+					}
+					elseif (file_exists($file_path = $path.'config/database.php'))
+					{
+						include($file_path);
+					}
+				}
+			}
+		}
+
+		if ( ! isset($db) OR count($db) === 0)
+		{
+			show_error('No database connection settings were found in the database config file.');
+		}
+
+		if ($params !== '')
+		{
+			$active_group = $params;
+		}
+
+		if ( ! isset($active_group))
+		{
+			show_error('You have not specified a database connection group via $active_group in your config/database.php file.');
+		}
+		elseif ( ! isset($db[$active_group]))
+		{
+			show_error('You have specified an invalid database connection group ('.$active_group.') in your config/database.php file.');
+		}
+
+		$params = $db[$active_group];
+	}
+	elseif (is_string($params))
+	{
+		/**
+		 * Parse the URL from the DSN string
+		 * Database settings can be passed as discreet
+		 * parameters or as a data source name in the first
+		 * parameter. DSNs must have this prototype:
+		 * $dsn = 'driver://username:password@hostname/database';
+		 */
+		if (($dsn = @parse_url($params)) === FALSE)
+		{
+			show_error('Invalid DB Connection String');
+		}
+
+		$params = array(
+			'dbdriver'	=> $dsn['scheme'],
+			'hostname'	=> isset($dsn['host']) ? rawurldecode($dsn['host']) : '',
+			'port'		=> isset($dsn['port']) ? rawurldecode($dsn['port']) : '',
+			'username'	=> isset($dsn['user']) ? rawurldecode($dsn['user']) : '',
+			'password'	=> isset($dsn['pass']) ? rawurldecode($dsn['pass']) : '',
+			'database'	=> isset($dsn['path']) ? rawurldecode(substr($dsn['path'], 1)) : ''
+		);
+
+		// Were additional config items set?
+		if (isset($dsn['query']))
+		{
+			parse_str($dsn['query'], $extra);
+
+			foreach ($extra as $key => $val)
+			{
+				if (is_string($val) && in_array(strtoupper($val), array('TRUE', 'FALSE', 'NULL')))
+				{
+					$val = var_export($val, TRUE);
+				}
+
+				$params[$key] = $val;
+			}
+		}
+	}
+
+	// No DB specified yet? Beat them senseless...
+	if (empty($params['dbdriver']))
+	{
+		show_error('You have not selected a database type to connect to.');
+	}
+
+	// Load the DB classes. Note: Since the query builder class is optional
+	// we need to dynamically create a class that extends proper parent class
+	// based on whether we're using the query builder class or not.
+	if ($query_builder_override !== NULL)
+	{
+		$query_builder = $query_builder_override;
+	}
+	// Backwards compatibility work-around for keeping the
+	// $active_record config variable working. Should be
+	// removed in v3.1
+	elseif ( ! isset($query_builder) && isset($active_record))
+	{
+		$query_builder = $active_record;
+	}
+
+	require_once(BASEPATH.'database/DB_driver.php');
+
+	if ( ! isset($query_builder) OR $query_builder === TRUE)
+	{
+		require_once(BASEPATH.'database/DB_query_builder.php');
+		if ( ! class_exists('CI_DB', FALSE))
+		{
+			/**
+			 * CI_DB
+			 *
+			 * Acts as an alias for both CI_DB_driver and CI_DB_query_builder.
+			 *
+			 * @see	CI_DB_query_builder
+			 * @see	CI_DB_driver
+			 */
+			class CI_DB extends CI_DB_query_builder { }
+		}
+	}
+	elseif ( ! class_exists('CI_DB', FALSE))
+	{
+		/**
+		 * @ignore
+		 */
+		class CI_DB extends CI_DB_driver { }
+	}
+
+	// Load the DB driver
+	$driver_file = BASEPATH.'database/drivers/'.$params['dbdriver'].'/'.$params['dbdriver'].'_driver.php';
+
+	file_exists($driver_file) OR show_error('Invalid DB driver');
+	require_once($driver_file);
+
+	// Instantiate the DB adapter
+	$driver = 'CI_DB_'.$params['dbdriver'].'_driver';
+	$DB = new $driver($params);
+
+	// Check for a subdriver
+	if ( ! empty($DB->subdriver))
+	{
+		$driver_file = BASEPATH.'database/drivers/'.$DB->dbdriver.'/subdrivers/'.$DB->dbdriver.'_'.$DB->subdriver.'_driver.php';
+
+		if (file_exists($driver_file))
+		{
+			require_once($driver_file);
+			$driver = 'CI_DB_'.$DB->dbdriver.'_'.$DB->subdriver.'_driver';
+			$DB = new $driver($params);
+		}
+	}
+
+	$DB->initialize();
+	return $DB;
+}
diff --git a/system/database/DB_cache.php b/system/database/DB_cache.php
new file mode 100644
index 0000000..d05ebb2
--- /dev/null
+++ b/system/database/DB_cache.php
@@ -0,0 +1,222 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Database Cache Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_Cache {
+
+	/**
+	 * CI Singleton
+	 *
+	 * @var	object
+	 */
+	public $CI;
+
+	/**
+	 * Database object
+	 *
+	 * Allows passing of DB object so that multiple database connections
+	 * and returned DB objects can be supported.
+	 *
+	 * @var	object
+	 */
+	public $db;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * @param	object	&$db
+	 * @return	void
+	 */
+	public function __construct(&$db)
+	{
+		// Assign the main CI object to $this->CI and load the file helper since we use it a lot
+		$this->CI =& get_instance();
+		$this->db =& $db;
+		$this->CI->load->helper('file');
+
+		$this->check_path();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Cache Directory Path
+	 *
+	 * @param	string	$path	Path to the cache directory
+	 * @return	bool
+	 */
+	public function check_path($path = '')
+	{
+		if ($path === '')
+		{
+			if ($this->db->cachedir === '')
+			{
+				return $this->db->cache_off();
+			}
+
+			$path = $this->db->cachedir;
+		}
+
+		// Add a trailing slash to the path if needed
+		$path = realpath($path)
+			? rtrim(realpath($path), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR
+			: rtrim($path, '/').'/';
+
+		if ( ! is_dir($path))
+		{
+			log_message('debug', 'DB cache path error: '.$path);
+
+			// If the path is wrong we'll turn off caching
+			return $this->db->cache_off();
+		}
+
+		if ( ! is_really_writable($path))
+		{
+			log_message('debug', 'DB cache dir not writable: '.$path);
+
+			// If the path is not really writable we'll turn off caching
+			return $this->db->cache_off();
+		}
+
+		$this->db->cachedir = $path;
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Retrieve a cached query
+	 *
+	 * The URI being requested will become the name of the cache sub-folder.
+	 * An MD5 hash of the SQL statement will become the cache file name.
+	 *
+	 * @param	string	$sql
+	 * @return	string
+	 */
+	public function read($sql)
+	{
+		$segment_one = ($this->CI->uri->segment(1) == FALSE) ? 'default' : $this->CI->uri->segment(1);
+		$segment_two = ($this->CI->uri->segment(2) == FALSE) ? 'index' : $this->CI->uri->segment(2);
+		$filepath = $this->db->cachedir.$segment_one.'+'.$segment_two.'/'.md5($sql);
+
+		if ( ! is_file($filepath) OR FALSE === ($cachedata = file_get_contents($filepath)))
+		{
+			return FALSE;
+		}
+
+		return unserialize($cachedata);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Write a query to a cache file
+	 *
+	 * @param	string	$sql
+	 * @param	object	$object
+	 * @return	bool
+	 */
+	public function write($sql, $object)
+	{
+		$segment_one = ($this->CI->uri->segment(1) == FALSE) ? 'default' : $this->CI->uri->segment(1);
+		$segment_two = ($this->CI->uri->segment(2) == FALSE) ? 'index' : $this->CI->uri->segment(2);
+		$dir_path = $this->db->cachedir.$segment_one.'+'.$segment_two.'/';
+		$filename = md5($sql);
+
+		if ( ! is_dir($dir_path) && ! @mkdir($dir_path, 0750))
+		{
+			return FALSE;
+		}
+
+		if (write_file($dir_path.$filename, serialize($object)) === FALSE)
+		{
+			return FALSE;
+		}
+
+		chmod($dir_path.$filename, 0640);
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete cache files within a particular directory
+	 *
+	 * @param	string	$segment_one
+	 * @param	string	$segment_two
+	 * @return	void
+	 */
+	public function delete($segment_one = '', $segment_two = '')
+	{
+		if ($segment_one === '')
+		{
+			$segment_one  = ($this->CI->uri->segment(1) == FALSE) ? 'default' : $this->CI->uri->segment(1);
+		}
+
+		if ($segment_two === '')
+		{
+			$segment_two = ($this->CI->uri->segment(2) == FALSE) ? 'index' : $this->CI->uri->segment(2);
+		}
+
+		$dir_path = $this->db->cachedir.$segment_one.'+'.$segment_two.'/';
+		delete_files($dir_path, TRUE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete all existing cache files
+	 *
+	 * @return	void
+	 */
+	public function delete_all()
+	{
+		delete_files($this->db->cachedir, TRUE, TRUE);
+	}
+
+}
diff --git a/system/database/DB_driver.php b/system/database/DB_driver.php
new file mode 100644
index 0000000..522f1bb
--- /dev/null
+++ b/system/database/DB_driver.php
@@ -0,0 +1,1999 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Database Driver Class
+ *
+ * This is the platform-independent base DB implementation class.
+ * This class will not be called directly. Rather, the adapter
+ * class for the specific database will extend and instantiate it.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+abstract class CI_DB_driver {
+
+	/**
+	 * Data Source Name / Connect string
+	 *
+	 * @var	string
+	 */
+	public $dsn;
+
+	/**
+	 * Username
+	 *
+	 * @var	string
+	 */
+	public $username;
+
+	/**
+	 * Password
+	 *
+	 * @var	string
+	 */
+	public $password;
+
+	/**
+	 * Hostname
+	 *
+	 * @var	string
+	 */
+	public $hostname;
+
+	/**
+	 * Database name
+	 *
+	 * @var	string
+	 */
+	public $database;
+
+	/**
+	 * Database driver
+	 *
+	 * @var	string
+	 */
+	public $dbdriver		= 'mysqli';
+
+	/**
+	 * Sub-driver
+	 *
+	 * @used-by	CI_DB_pdo_driver
+	 * @var	string
+	 */
+	public $subdriver;
+
+	/**
+	 * Table prefix
+	 *
+	 * @var	string
+	 */
+	public $dbprefix		= '';
+
+	/**
+	 * Character set
+	 *
+	 * @var	string
+	 */
+	public $char_set		= 'utf8';
+
+	/**
+	 * Collation
+	 *
+	 * @var	string
+	 */
+	public $dbcollat		= 'utf8_general_ci';
+
+	/**
+	 * Encryption flag/data
+	 *
+	 * @var	mixed
+	 */
+	public $encrypt			= FALSE;
+
+	/**
+	 * Swap Prefix
+	 *
+	 * @var	string
+	 */
+	public $swap_pre		= '';
+
+	/**
+	 * Database port
+	 *
+	 * @var	int
+	 */
+	public $port			= NULL;
+
+	/**
+	 * Persistent connection flag
+	 *
+	 * @var	bool
+	 */
+	public $pconnect		= FALSE;
+
+	/**
+	 * Connection ID
+	 *
+	 * @var	object|resource
+	 */
+	public $conn_id			= FALSE;
+
+	/**
+	 * Result ID
+	 *
+	 * @var	object|resource
+	 */
+	public $result_id		= FALSE;
+
+	/**
+	 * Debug flag
+	 *
+	 * Whether to display error messages.
+	 *
+	 * @var	bool
+	 */
+	public $db_debug		= FALSE;
+
+	/**
+	 * Benchmark time
+	 *
+	 * @var	int
+	 */
+	public $benchmark		= 0;
+
+	/**
+	 * Executed queries count
+	 *
+	 * @var	int
+	 */
+	public $query_count		= 0;
+
+	/**
+	 * Bind marker
+	 *
+	 * Character used to identify values in a prepared statement.
+	 *
+	 * @var	string
+	 */
+	public $bind_marker		= '?';
+
+	/**
+	 * Save queries flag
+	 *
+	 * Whether to keep an in-memory history of queries for debugging purposes.
+	 *
+	 * @var	bool
+	 */
+	public $save_queries		= TRUE;
+
+	/**
+	 * Queries list
+	 *
+	 * @see	CI_DB_driver::$save_queries
+	 * @var	string[]
+	 */
+	public $queries			= array();
+
+	/**
+	 * Query times
+	 *
+	 * A list of times that queries took to execute.
+	 *
+	 * @var	array
+	 */
+	public $query_times		= array();
+
+	/**
+	 * Data cache
+	 *
+	 * An internal generic value cache.
+	 *
+	 * @var	array
+	 */
+	public $data_cache		= array();
+
+	/**
+	 * Transaction enabled flag
+	 *
+	 * @var	bool
+	 */
+	public $trans_enabled		= TRUE;
+
+	/**
+	 * Strict transaction mode flag
+	 *
+	 * @var	bool
+	 */
+	public $trans_strict		= TRUE;
+
+	/**
+	 * Transaction depth level
+	 *
+	 * @var	int
+	 */
+	protected $_trans_depth		= 0;
+
+	/**
+	 * Transaction status flag
+	 *
+	 * Used with transactions to determine if a rollback should occur.
+	 *
+	 * @var	bool
+	 */
+	protected $_trans_status	= TRUE;
+
+	/**
+	 * Transaction failure flag
+	 *
+	 * Used with transactions to determine if a transaction has failed.
+	 *
+	 * @var	bool
+	 */
+	protected $_trans_failure	= FALSE;
+
+	/**
+	 * Cache On flag
+	 *
+	 * @var	bool
+	 */
+	public $cache_on		= FALSE;
+
+	/**
+	 * Cache directory path
+	 *
+	 * @var	bool
+	 */
+	public $cachedir		= '';
+
+	/**
+	 * Cache auto-delete flag
+	 *
+	 * @var	bool
+	 */
+	public $cache_autodel		= FALSE;
+
+	/**
+	 * DB Cache object
+	 *
+	 * @see	CI_DB_cache
+	 * @var	object
+	 */
+	public $CACHE;
+
+	/**
+	 * Protect identifiers flag
+	 *
+	 * @var	bool
+	 */
+	protected $_protect_identifiers		= TRUE;
+
+	/**
+	 * List of reserved identifiers
+	 *
+	 * Identifiers that must NOT be escaped.
+	 *
+	 * @var	string[]
+	 */
+	protected $_reserved_identifiers	= array('*');
+
+	/**
+	 * Identifier escape character
+	 *
+	 * @var	string
+	 */
+	protected $_escape_char = '"';
+
+	/**
+	 * ESCAPE statement string
+	 *
+	 * @var	string
+	 */
+	protected $_like_escape_str = " ESCAPE '%s' ";
+
+	/**
+	 * ESCAPE character
+	 *
+	 * @var	string
+	 */
+	protected $_like_escape_chr = '!';
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('RAND()', 'RAND(%d)');
+
+	/**
+	 * COUNT string
+	 *
+	 * @used-by	CI_DB_driver::count_all()
+	 * @used-by	CI_DB_query_builder::count_all_results()
+	 *
+	 * @var	string
+	 */
+	protected $_count_string = 'SELECT COUNT(*) AS ';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		if (is_array($params))
+		{
+			foreach ($params as $key => $val)
+			{
+				$this->$key = $val;
+			}
+		}
+
+		log_message('info', 'Database Driver Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize Database Settings
+	 *
+	 * @return	bool
+	 */
+	public function initialize()
+	{
+		/* If an established connection is available, then there's
+		 * no need to connect and select the database.
+		 *
+		 * Depending on the database driver, conn_id can be either
+		 * boolean TRUE, a resource or an object.
+		 */
+		if ($this->conn_id)
+		{
+			return TRUE;
+		}
+
+		// ----------------------------------------------------------------
+
+		// Connect to the database and set the connection ID
+		$this->conn_id = $this->db_connect($this->pconnect);
+
+		// No connection resource? Check if there is a failover else throw an error
+		if ( ! $this->conn_id)
+		{
+			// Check if there is a failover set
+			if ( ! empty($this->failover) && is_array($this->failover))
+			{
+				// Go over all the failovers
+				foreach ($this->failover as $failover)
+				{
+					// Replace the current settings with those of the failover
+					foreach ($failover as $key => $val)
+					{
+						$this->$key = $val;
+					}
+
+					// Try to connect
+					$this->conn_id = $this->db_connect($this->pconnect);
+
+					// If a connection is made break the foreach loop
+					if ($this->conn_id)
+					{
+						break;
+					}
+				}
+			}
+
+			// We still don't have a connection?
+			if ( ! $this->conn_id)
+			{
+				log_message('error', 'Unable to connect to the database');
+
+				if ($this->db_debug)
+				{
+					$this->display_error('db_unable_to_connect');
+				}
+
+				return FALSE;
+			}
+		}
+
+		// Now we set the character set and that's all
+		return $this->db_set_charset($this->char_set);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * DB connect
+	 *
+	 * This is just a dummy method that all drivers will override.
+	 *
+	 * @return	mixed
+	 */
+	public function db_connect()
+	{
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Persistent database connection
+	 *
+	 * @return	mixed
+	 */
+	public function db_pconnect()
+	{
+		return $this->db_connect(TRUE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Reconnect
+	 *
+	 * Keep / reestablish the db connection if no queries have been
+	 * sent for a length of time exceeding the server's idle timeout.
+	 *
+	 * This is just a dummy method to allow drivers without such
+	 * functionality to not declare it, while others will override it.
+	 *
+	 * @return	void
+	 */
+	public function reconnect()
+	{
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Select database
+	 *
+	 * This is just a dummy method to allow drivers without such
+	 * functionality to not declare it, while others will override it.
+	 *
+	 * @return	bool
+	 */
+	public function db_select()
+	{
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Last error
+	 *
+	 * @return	array
+	 */
+	public function error()
+	{
+		return array('code' => NULL, 'message' => NULL);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set client character set
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function db_set_charset($charset)
+	{
+		if (method_exists($this, '_db_set_charset') && ! $this->_db_set_charset($charset))
+		{
+			log_message('error', 'Unable to set database connection charset: '.$charset);
+
+			if ($this->db_debug)
+			{
+				$this->display_error('db_unable_to_set_charset', $charset);
+			}
+
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * The name of the platform in use (mysql, mssql, etc...)
+	 *
+	 * @return	string
+	 */
+	public function platform()
+	{
+		return $this->dbdriver;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database version number
+	 *
+	 * Returns a string containing the version of the database being used.
+	 * Most drivers will override this method.
+	 *
+	 * @return	string
+	 */
+	public function version()
+	{
+		if (isset($this->data_cache['version']))
+		{
+			return $this->data_cache['version'];
+		}
+
+		if (FALSE === ($sql = $this->_version()))
+		{
+			return ($this->db_debug) ? $this->display_error('db_unsupported_function') : FALSE;
+		}
+
+		$query = $this->query($sql)->row();
+		return $this->data_cache['version'] = $query->ver;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Version number query string
+	 *
+	 * @return	string
+	 */
+	protected function _version()
+	{
+		return 'SELECT VERSION() AS ver';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Execute the query
+	 *
+	 * Accepts an SQL string as input and returns a result object upon
+	 * successful execution of a "read" type query. Returns boolean TRUE
+	 * upon successful execution of a "write" type query. Returns boolean
+	 * FALSE upon failure, and if the $db_debug variable is set to TRUE
+	 * will raise an error.
+	 *
+	 * @param	string	$sql
+	 * @param	array	$binds = FALSE		An array of binding data
+	 * @param	bool	$return_object = NULL
+	 * @return	mixed
+	 */
+	public function query($sql, $binds = FALSE, $return_object = NULL)
+	{
+		if ($sql === '')
+		{
+			log_message('error', 'Invalid query: '.$sql);
+			return ($this->db_debug) ? $this->display_error('db_invalid_query') : FALSE;
+		}
+		elseif ( ! is_bool($return_object))
+		{
+			$return_object = ! $this->is_write_type($sql);
+		}
+
+		// Verify table prefix and replace if necessary
+		if ($this->dbprefix !== '' && $this->swap_pre !== '' && $this->dbprefix !== $this->swap_pre)
+		{
+			$sql = preg_replace('/(\W)'.$this->swap_pre.'(\S+?)/', '\\1'.$this->dbprefix.'\\2', $sql);
+		}
+
+		// Compile binds if needed
+		if ($binds !== FALSE)
+		{
+			$sql = $this->compile_binds($sql, $binds);
+		}
+
+		// Is query caching enabled? If the query is a "read type"
+		// we will load the caching class and return the previously
+		// cached query if it exists
+		if ($this->cache_on === TRUE && $return_object === TRUE && $this->_cache_init())
+		{
+			$this->load_rdriver();
+			if (FALSE !== ($cache = $this->CACHE->read($sql)))
+			{
+				return $cache;
+			}
+		}
+
+		// Save the query for debugging
+		if ($this->save_queries === TRUE)
+		{
+			$this->queries[] = $sql;
+		}
+
+		// Start the Query Timer
+		$time_start = microtime(TRUE);
+
+		// Run the Query
+		if (FALSE === ($this->result_id = $this->simple_query($sql)))
+		{
+			if ($this->save_queries === TRUE)
+			{
+				$this->query_times[] = 0;
+			}
+
+			// This will trigger a rollback if transactions are being used
+			if ($this->_trans_depth !== 0)
+			{
+				$this->_trans_status = FALSE;
+			}
+
+			// Grab the error now, as we might run some additional queries before displaying the error
+			$error = $this->error();
+
+			// Log errors
+			log_message('error', 'Query error: '.$error['message'].' - Invalid query: '.$sql);
+
+			if ($this->db_debug)
+			{
+				// We call this function in order to roll-back queries
+				// if transactions are enabled. If we don't call this here
+				// the error message will trigger an exit, causing the
+				// transactions to remain in limbo.
+				while ($this->_trans_depth !== 0)
+				{
+					$trans_depth = $this->_trans_depth;
+					$this->trans_complete();
+					if ($trans_depth === $this->_trans_depth)
+					{
+						log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
+						break;
+					}
+				}
+
+				// Display errors
+				return $this->display_error(array('Error Number: '.$error['code'], $error['message'], $sql));
+			}
+
+			return FALSE;
+		}
+
+		// Stop and aggregate the query time results
+		$time_end = microtime(TRUE);
+		$this->benchmark += $time_end - $time_start;
+
+		if ($this->save_queries === TRUE)
+		{
+			$this->query_times[] = $time_end - $time_start;
+		}
+
+		// Increment the query counter
+		$this->query_count++;
+
+		// Will we have a result object instantiated? If not - we'll simply return TRUE
+		if ($return_object !== TRUE)
+		{
+			// If caching is enabled we'll auto-cleanup any existing files related to this particular URI
+			if ($this->cache_on === TRUE && $this->cache_autodel === TRUE && $this->_cache_init())
+			{
+				$this->CACHE->delete();
+			}
+
+			return TRUE;
+		}
+
+		// Load and instantiate the result driver
+		$driver		= $this->load_rdriver();
+		$RES		= new $driver($this);
+
+		// Is query caching enabled? If so, we'll serialize the
+		// result object and save it to a cache file.
+		if ($this->cache_on === TRUE && $this->_cache_init())
+		{
+			// We'll create a new instance of the result object
+			// only without the platform specific driver since
+			// we can't use it with cached data (the query result
+			// resource ID won't be any good once we've cached the
+			// result object, so we'll have to compile the data
+			// and save it)
+			$CR = new CI_DB_result($this);
+			$CR->result_object	= $RES->result_object();
+			$CR->result_array	= $RES->result_array();
+			$CR->num_rows		= $RES->num_rows();
+
+			// Reset these since cached objects can not utilize resource IDs.
+			$CR->conn_id		= NULL;
+			$CR->result_id		= NULL;
+
+			$this->CACHE->write($sql, $CR);
+		}
+
+		return $RES;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Load the result drivers
+	 *
+	 * @return	string	the name of the result class
+	 */
+	public function load_rdriver()
+	{
+		$driver = 'CI_DB_'.$this->dbdriver.'_result';
+
+		if ( ! class_exists($driver, FALSE))
+		{
+			require_once(BASEPATH.'database/DB_result.php');
+			require_once(BASEPATH.'database/drivers/'.$this->dbdriver.'/'.$this->dbdriver.'_result.php');
+		}
+
+		return $driver;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Simple Query
+	 * This is a simplified version of the query() function. Internally
+	 * we only use it when running transaction commands since they do
+	 * not require all the features of the main query() function.
+	 *
+	 * @param	string	the sql query
+	 * @return	mixed
+	 */
+	public function simple_query($sql)
+	{
+		if ( ! $this->conn_id)
+		{
+			if ( ! $this->initialize())
+			{
+				return FALSE;
+			}
+		}
+
+		return $this->_execute($sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Disable Transactions
+	 * This permits transactions to be disabled at run-time.
+	 *
+	 * @return	void
+	 */
+	public function trans_off()
+	{
+		$this->trans_enabled = FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Enable/disable Transaction Strict Mode
+	 *
+	 * When strict mode is enabled, if you are running multiple groups of
+	 * transactions, if one group fails all subsequent groups will be
+	 * rolled back.
+	 *
+	 * If strict mode is disabled, each group is treated autonomously,
+	 * meaning a failure of one group will not affect any others
+	 *
+	 * @param	bool	$mode = TRUE
+	 * @return	void
+	 */
+	public function trans_strict($mode = TRUE)
+	{
+		$this->trans_strict = is_bool($mode) ? $mode : TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Start Transaction
+	 *
+	 * @param	bool	$test_mode = FALSE
+	 * @return	bool
+	 */
+	public function trans_start($test_mode = FALSE)
+	{
+		if ( ! $this->trans_enabled)
+		{
+			return FALSE;
+		}
+
+		return $this->trans_begin($test_mode);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Complete Transaction
+	 *
+	 * @return	bool
+	 */
+	public function trans_complete()
+	{
+		if ( ! $this->trans_enabled)
+		{
+			return FALSE;
+		}
+
+		// The query() function will set this flag to FALSE in the event that a query failed
+		if ($this->_trans_status === FALSE OR $this->_trans_failure === TRUE)
+		{
+			$this->trans_rollback();
+
+			// If we are NOT running in strict mode, we will reset
+			// the _trans_status flag so that subsequent groups of
+			// transactions will be permitted.
+			if ($this->trans_strict === FALSE)
+			{
+				$this->_trans_status = TRUE;
+			}
+
+			log_message('debug', 'DB Transaction Failure');
+			return FALSE;
+		}
+
+		return $this->trans_commit();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Lets you retrieve the transaction flag to determine if it has failed
+	 *
+	 * @return	bool
+	 */
+	public function trans_status()
+	{
+		return $this->_trans_status;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns TRUE if a transaction is currently active
+	 *
+	 * @return	bool
+	 */
+	public function trans_active()
+	{
+		return (bool) $this->_trans_depth;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Begin Transaction
+	 *
+	 * @param	bool	$test_mode
+	 * @return	bool
+	 */
+	public function trans_begin($test_mode = FALSE)
+	{
+		if ( ! $this->trans_enabled)
+		{
+			return FALSE;
+		}
+		// When transactions are nested we only begin/commit/rollback the outermost ones
+		elseif ($this->_trans_depth > 0)
+		{
+			$this->_trans_depth++;
+			return TRUE;
+		}
+
+		// Reset the transaction failure flag.
+		// If the $test_mode flag is set to TRUE transactions will be rolled back
+		// even if the queries produce a successful result.
+		$this->_trans_failure = ($test_mode === TRUE);
+
+		if ($this->_trans_begin())
+		{
+			$this->_trans_status = TRUE;
+			$this->_trans_depth++;
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Commit Transaction
+	 *
+	 * @return	bool
+	 */
+	public function trans_commit()
+	{
+		if ( ! $this->trans_enabled OR $this->_trans_depth === 0)
+		{
+			return FALSE;
+		}
+		// When transactions are nested we only begin/commit/rollback the outermost ones
+		elseif ($this->_trans_depth > 1 OR $this->_trans_commit())
+		{
+			$this->_trans_depth--;
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rollback Transaction
+	 *
+	 * @return	bool
+	 */
+	public function trans_rollback()
+	{
+		if ( ! $this->trans_enabled OR $this->_trans_depth === 0)
+		{
+			return FALSE;
+		}
+		// When transactions are nested we only begin/commit/rollback the outermost ones
+		elseif ($this->_trans_depth > 1 OR $this->_trans_rollback())
+		{
+			$this->_trans_depth--;
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile Bindings
+	 *
+	 * @param	string	the sql statement
+	 * @param	array	an array of bind data
+	 * @return	string
+	 */
+	public function compile_binds($sql, $binds)
+	{
+		if (empty($this->bind_marker) OR strpos($sql, $this->bind_marker) === FALSE)
+		{
+			return $sql;
+		}
+		elseif ( ! is_array($binds))
+		{
+			$binds = array($binds);
+			$bind_count = 1;
+		}
+		else
+		{
+			// Make sure we're using numeric keys
+			$binds = array_values($binds);
+			$bind_count = count($binds);
+		}
+
+		// We'll need the marker length later
+		$ml = strlen($this->bind_marker);
+
+		// Make sure not to replace a chunk inside a string that happens to match the bind marker
+		if ($c = preg_match_all("/'[^']*'|\"[^\"]*\"/i", $sql, $matches))
+		{
+			$c = preg_match_all('/'.preg_quote($this->bind_marker, '/').'/i',
+				str_replace($matches[0],
+					str_replace($this->bind_marker, str_repeat(' ', $ml), $matches[0]),
+					$sql, $c),
+				$matches, PREG_OFFSET_CAPTURE);
+
+			// Bind values' count must match the count of markers in the query
+			if ($bind_count !== $c)
+			{
+				return $sql;
+			}
+		}
+		elseif (($c = preg_match_all('/'.preg_quote($this->bind_marker, '/').'/i', $sql, $matches, PREG_OFFSET_CAPTURE)) !== $bind_count)
+		{
+			return $sql;
+		}
+
+		do
+		{
+			$c--;
+			$escaped_value = $this->escape($binds[$c]);
+			if (is_array($escaped_value))
+			{
+				$escaped_value = '('.implode(',', $escaped_value).')';
+			}
+			$sql = substr_replace($sql, $escaped_value, $matches[0][$c][1], $ml);
+		}
+		while ($c !== 0);
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Determines if a query is a "write" type.
+	 *
+	 * @param	string	An SQL query string
+	 * @return	bool
+	 */
+	public function is_write_type($sql)
+	{
+		return (bool) preg_match('/^\s*"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX|MERGE)\s/i', $sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Calculate the aggregate query elapsed time
+	 *
+	 * @param	int	The number of decimal places
+	 * @return	string
+	 */
+	public function elapsed_time($decimals = 6)
+	{
+		return number_format($this->benchmark, $decimals);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns the total number of queries
+	 *
+	 * @return	int
+	 */
+	public function total_queries()
+	{
+		return $this->query_count;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns the last query that was executed
+	 *
+	 * @return	string
+	 */
+	public function last_query()
+	{
+		return end($this->queries);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * "Smart" Escape String
+	 *
+	 * Escapes data based on type
+	 * Sets boolean and null types
+	 *
+	 * @param	string
+	 * @return	mixed
+	 */
+	public function escape($str)
+	{
+		if (is_array($str))
+		{
+			$str = array_map(array(&$this, 'escape'), $str);
+			return $str;
+		}
+		elseif (is_string($str) OR (is_object($str) && method_exists($str, '__toString')))
+		{
+			return "'".$this->escape_str($str)."'";
+		}
+		elseif (is_bool($str))
+		{
+			return ($str === FALSE) ? 0 : 1;
+		}
+		elseif ($str === NULL)
+		{
+			return 'NULL';
+		}
+
+		return $str;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Escape String
+	 *
+	 * @param	string|string[]	$str	Input string
+	 * @param	bool	$like	Whether or not the string will be used in a LIKE condition
+	 * @return	string
+	 */
+	public function escape_str($str, $like = FALSE)
+	{
+		if (is_array($str))
+		{
+			foreach ($str as $key => $val)
+			{
+				$str[$key] = $this->escape_str($val, $like);
+			}
+
+			return $str;
+		}
+
+		$str = $this->_escape_str($str);
+
+		// escape LIKE condition wildcards
+		if ($like === TRUE)
+		{
+			return str_replace(
+				array($this->_like_escape_chr, '%', '_'),
+				array($this->_like_escape_chr.$this->_like_escape_chr, $this->_like_escape_chr.'%', $this->_like_escape_chr.'_'),
+				$str
+			);
+		}
+
+		return $str;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Escape LIKE String
+	 *
+	 * Calls the individual driver for platform
+	 * specific escaping for LIKE conditions
+	 *
+	 * @param	string|string[]
+	 * @return	mixed
+	 */
+	public function escape_like_str($str)
+	{
+		return $this->escape_str($str, TRUE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Platform-dependent string escape
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _escape_str($str)
+	{
+		return str_replace("'", "''", remove_invisible_characters($str, FALSE));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Primary
+	 *
+	 * Retrieves the primary key. It assumes that the row in the first
+	 * position is the primary key
+	 *
+	 * @param	string	$table	Table name
+	 * @return	string
+	 */
+	public function primary($table)
+	{
+		$fields = $this->list_fields($table);
+		return is_array($fields) ? current($fields) : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * "Count All" query
+	 *
+	 * Generates a platform-specific query string that counts all records in
+	 * the specified database
+	 *
+	 * @param	string
+	 * @return	int
+	 */
+	public function count_all($table = '')
+	{
+		if ($table === '')
+		{
+			return 0;
+		}
+
+		$query = $this->query($this->_count_string.$this->escape_identifiers('numrows').' FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE));
+		if ($query->num_rows() === 0)
+		{
+			return 0;
+		}
+
+		$query = $query->row();
+		$this->_reset_select();
+		return (int) $query->numrows;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an array of table names
+	 *
+	 * @param	string	$constrain_by_prefix = FALSE
+	 * @return	array
+	 */
+	public function list_tables($constrain_by_prefix = FALSE)
+	{
+		// Is there a cached result?
+		if (isset($this->data_cache['table_names']))
+		{
+			return $this->data_cache['table_names'];
+		}
+
+		if (FALSE === ($sql = $this->_list_tables($constrain_by_prefix)))
+		{
+			return ($this->db_debug) ? $this->display_error('db_unsupported_function') : FALSE;
+		}
+
+		$this->data_cache['table_names'] = array();
+		$query = $this->query($sql);
+
+		foreach ($query->result_array() as $row)
+		{
+			// Do we know from which column to get the table name?
+			if ( ! isset($key))
+			{
+				if (isset($row['table_name']))
+				{
+					$key = 'table_name';
+				}
+				elseif (isset($row['TABLE_NAME']))
+				{
+					$key = 'TABLE_NAME';
+				}
+				else
+				{
+					/* We have no other choice but to just get the first element's key.
+					 * Due to array_shift() accepting its argument by reference, if
+					 * E_STRICT is on, this would trigger a warning. So we'll have to
+					 * assign it first.
+					 */
+					$key = array_keys($row);
+					$key = array_shift($key);
+				}
+			}
+
+			$this->data_cache['table_names'][] = $row[$key];
+		}
+
+		return $this->data_cache['table_names'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Determine if a particular table exists
+	 *
+	 * @param	string	$table_name
+	 * @return	bool
+	 */
+	public function table_exists($table_name)
+	{
+		return in_array($this->protect_identifiers($table_name, TRUE, FALSE, FALSE), $this->list_tables());
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * @param	string	$table	Table name
+	 * @return	array
+	 */
+	public function list_fields($table)
+	{
+		if (FALSE === ($sql = $this->_list_columns($table)))
+		{
+			return ($this->db_debug) ? $this->display_error('db_unsupported_function') : FALSE;
+		}
+
+		$query = $this->query($sql);
+		$fields = array();
+
+		foreach ($query->result_array() as $row)
+		{
+			// Do we know from where to get the column's name?
+			if ( ! isset($key))
+			{
+				if (isset($row['column_name']))
+				{
+					$key = 'column_name';
+				}
+				elseif (isset($row['COLUMN_NAME']))
+				{
+					$key = 'COLUMN_NAME';
+				}
+				else
+				{
+					// We have no other choice but to just get the first element's key.
+					$key = key($row);
+				}
+			}
+
+			$fields[] = $row[$key];
+		}
+
+		return $fields;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Determine if a particular field exists
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	bool
+	 */
+	public function field_exists($field_name, $table_name)
+	{
+		return in_array($field_name, $this->list_fields($table_name));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table	the table name
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		$query = $this->query($this->_field_data($this->protect_identifiers($table, TRUE, NULL, FALSE)));
+		return ($query) ? $query->field_data() : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Escape the SQL Identifiers
+	 *
+	 * This function escapes column and table names
+	 *
+	 * @param	mixed
+	 * @return	mixed
+	 */
+	public function escape_identifiers($item)
+	{
+		if ($this->_escape_char === '' OR empty($item) OR in_array($item, $this->_reserved_identifiers))
+		{
+			return $item;
+		}
+		elseif (is_array($item))
+		{
+			foreach ($item as $key => $value)
+			{
+				$item[$key] = $this->escape_identifiers($value);
+			}
+
+			return $item;
+		}
+		// Avoid breaking functions and literal values inside queries
+		elseif (ctype_digit($item) OR $item[0] === "'" OR ($this->_escape_char !== '"' && $item[0] === '"') OR strpos($item, '(') !== FALSE)
+		{
+			return $item;
+		}
+
+		static $preg_ec = array();
+
+		if (empty($preg_ec))
+		{
+			if (is_array($this->_escape_char))
+			{
+				$preg_ec = array(
+					preg_quote($this->_escape_char[0], '/'),
+					preg_quote($this->_escape_char[1], '/'),
+					$this->_escape_char[0],
+					$this->_escape_char[1]
+				);
+			}
+			else
+			{
+				$preg_ec[0] = $preg_ec[1] = preg_quote($this->_escape_char, '/');
+				$preg_ec[2] = $preg_ec[3] = $this->_escape_char;
+			}
+		}
+
+		foreach ($this->_reserved_identifiers as $id)
+		{
+			if (strpos($item, '.'.$id) !== FALSE)
+			{
+				return preg_replace('/'.$preg_ec[0].'?([^'.$preg_ec[1].'\.]+)'.$preg_ec[1].'?\./i', $preg_ec[2].'$1'.$preg_ec[3].'.', $item);
+			}
+		}
+
+		return preg_replace('/'.$preg_ec[0].'?([^'.$preg_ec[1].'\.]+)'.$preg_ec[1].'?(\.)?/i', $preg_ec[2].'$1'.$preg_ec[3].'$2', $item);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Generate an insert string
+	 *
+	 * @param	string	the table upon which the query will be performed
+	 * @param	array	an associative array data of key/values
+	 * @return	string
+	 */
+	public function insert_string($table, $data)
+	{
+		$fields = $values = array();
+
+		foreach ($data as $key => $val)
+		{
+			$fields[] = $this->escape_identifiers($key);
+			$values[] = $this->escape($val);
+		}
+
+		return $this->_insert($this->protect_identifiers($table, TRUE, NULL, FALSE), $fields, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert statement
+	 *
+	 * Generates a platform-specific insert string from the supplied data
+	 *
+	 * @param	string	the table name
+	 * @param	array	the insert keys
+	 * @param	array	the insert values
+	 * @return	string
+	 */
+	protected function _insert($table, $keys, $values)
+	{
+		return 'INSERT INTO '.$table.' ('.implode(', ', $keys).') VALUES ('.implode(', ', $values).')';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Generate an update string
+	 *
+	 * @param	string	the table upon which the query will be performed
+	 * @param	array	an associative array data of key/values
+	 * @param	mixed	the "where" statement
+	 * @return	string
+	 */
+	public function update_string($table, $data, $where)
+	{
+		if (empty($where))
+		{
+			return FALSE;
+		}
+
+		$this->where($where);
+
+		$fields = array();
+		foreach ($data as $key => $val)
+		{
+			$fields[$this->protect_identifiers($key)] = $this->escape($val);
+		}
+
+		$sql = $this->_update($this->protect_identifiers($table, TRUE, NULL, FALSE), $fields);
+		$this->_reset_write();
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update statement
+	 *
+	 * Generates a platform-specific update string from the supplied data
+	 *
+	 * @param	string	the table name
+	 * @param	array	the update data
+	 * @return	string
+	 */
+	protected function _update($table, $values)
+	{
+		foreach ($values as $key => $val)
+		{
+			$valstr[] = $key.' = '.$val;
+		}
+
+		return 'UPDATE '.$table.' SET '.implode(', ', $valstr)
+			.$this->_compile_wh('qb_where')
+			.$this->_compile_order_by()
+			.($this->qb_limit !== FALSE ? ' LIMIT '.$this->qb_limit : '');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Tests whether the string has an SQL operator
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	protected function _has_operator($str)
+	{
+		return (bool) preg_match('/(<|>|!|=|\sIS NULL|\sIS NOT NULL|\sEXISTS|\sBETWEEN|\sLIKE|\sIN\s*\(|\s)/i', trim($str));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns the SQL string operator
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _get_operator($str)
+	{
+		static $_operators;
+
+		if (empty($_operators))
+		{
+			$_les = ($this->_like_escape_str !== '')
+				? '\s+'.preg_quote(trim(sprintf($this->_like_escape_str, $this->_like_escape_chr)), '/')
+				: '';
+			$_operators = array(
+				'\s*(?:<|>|!)?=\s*',             // =, <=, >=, !=
+				'\s*<>?\s*',                     // <, <>
+				'\s*>\s*',                       // >
+				'\s+IS NULL',                    // IS NULL
+				'\s+IS NOT NULL',                // IS NOT NULL
+				'\s+EXISTS\s*\(.*\)',        // EXISTS(sql)
+				'\s+NOT EXISTS\s*\(.*\)',    // NOT EXISTS(sql)
+				'\s+BETWEEN\s+',                 // BETWEEN value AND value
+				'\s+NOT BETWEEN\s+',             // NOT BETWEEN value AND value
+				'\s+IN\s*\(.*\)',            // IN(list)
+				'\s+NOT IN\s*\(.*\)',        // NOT IN (list)
+				'\s+LIKE\s+\S.*('.$_les.')?',    // LIKE 'expr'[ ESCAPE '%s']
+				'\s+NOT LIKE\s+\S.*('.$_les.')?' // NOT LIKE 'expr'[ ESCAPE '%s']
+			);
+
+		}
+
+		return preg_match('/'.implode('|', $_operators).'/i', $str, $match)
+			? $match[0] : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Enables a native PHP function to be run, using a platform agnostic wrapper.
+	 *
+	 * @param	string	$function	Function name
+	 * @return	mixed
+	 */
+	public function call_function($function)
+	{
+		$driver = ($this->dbdriver === 'postgre') ? 'pg_' : $this->dbdriver.'_';
+
+		if (FALSE === strpos($driver, $function))
+		{
+			$function = $driver.$function;
+		}
+
+		if ( ! function_exists($function))
+		{
+			return ($this->db_debug) ? $this->display_error('db_unsupported_function') : FALSE;
+		}
+
+		return (func_num_args() > 1)
+			? call_user_func_array($function, array_slice(func_get_args(), 1))
+			: call_user_func($function);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set Cache Directory Path
+	 *
+	 * @param	string	the path to the cache directory
+	 * @return	void
+	 */
+	public function cache_set_path($path = '')
+	{
+		$this->cachedir = $path;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Enable Query Caching
+	 *
+	 * @return	bool	cache_on value
+	 */
+	public function cache_on()
+	{
+		return $this->cache_on = TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Disable Query Caching
+	 *
+	 * @return	bool	cache_on value
+	 */
+	public function cache_off()
+	{
+		return $this->cache_on = FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete the cache files associated with a particular URI
+	 *
+	 * @param	string	$segment_one = ''
+	 * @param	string	$segment_two = ''
+	 * @return	bool
+	 */
+	public function cache_delete($segment_one = '', $segment_two = '')
+	{
+		return $this->_cache_init()
+			? $this->CACHE->delete($segment_one, $segment_two)
+			: FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete All cache files
+	 *
+	 * @return	bool
+	 */
+	public function cache_delete_all()
+	{
+		return $this->_cache_init()
+			? $this->CACHE->delete_all()
+			: FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize the Cache Class
+	 *
+	 * @return	bool
+	 */
+	protected function _cache_init()
+	{
+		if ( ! class_exists('CI_DB_Cache', FALSE))
+		{
+			require_once(BASEPATH.'database/DB_cache.php');
+		}
+		elseif (is_object($this->CACHE))
+		{
+			return TRUE;
+		}
+
+		$this->CACHE = new CI_DB_Cache($this); // pass db object to support multiple db connections and returned db objects
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close DB Connection
+	 *
+	 * @return	void
+	 */
+	public function close()
+	{
+		if ($this->conn_id)
+		{
+			$this->_close();
+			$this->conn_id = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close DB Connection
+	 *
+	 * This method would be overridden by most of the drivers.
+	 *
+	 * @return	void
+	 */
+	protected function _close()
+	{
+		$this->conn_id = FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Display an error message
+	 *
+	 * @param	string	the error message
+	 * @param	string	any "swap" values
+	 * @param	bool	whether to localize the message
+	 * @return	string	sends the application/views/errors/error_db.php template
+	 */
+	public function display_error($error = '', $swap = '', $native = FALSE)
+	{
+		$LANG =& load_class('Lang', 'core');
+		$LANG->load('db');
+
+		$heading = $LANG->line('db_error_heading');
+
+		if ($native === TRUE)
+		{
+			$message = (array) $error;
+		}
+		else
+		{
+			$message = is_array($error) ? $error : array(str_replace('%s', $swap, $LANG->line($error)));
+		}
+
+		// Find the most likely culprit of the error by going through
+		// the backtrace until the source file is no longer in the
+		// database folder.
+		$trace = debug_backtrace();
+		foreach ($trace as $call)
+		{
+			if (isset($call['file'], $call['class']))
+			{
+				// We'll need this on Windows, as APPPATH and BASEPATH will always use forward slashes
+				if (DIRECTORY_SEPARATOR !== '/')
+				{
+					$call['file'] = str_replace('\\', '/', $call['file']);
+				}
+
+				if (strpos($call['file'], BASEPATH.'database') === FALSE && strpos($call['class'], 'Loader') === FALSE)
+				{
+					// Found it - use a relative path for safety
+					$message[] = 'Filename: '.str_replace(array(APPPATH, BASEPATH), '', $call['file']);
+					$message[] = 'Line Number: '.$call['line'];
+					break;
+				}
+			}
+		}
+
+		$error =& load_class('Exceptions', 'core');
+		echo $error->show_error($heading, $message, 'error_db');
+		exit(8); // EXIT_DATABASE
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Protect Identifiers
+	 *
+	 * This function is used extensively by the Query Builder class, and by
+	 * a couple functions in this class.
+	 * It takes a column or table name (optionally with an alias) and inserts
+	 * the table prefix onto it. Some logic is necessary in order to deal with
+	 * column names that include the path. Consider a query like this:
+	 *
+	 * SELECT hostname.database.table.column AS c FROM hostname.database.table
+	 *
+	 * Or a query with aliasing:
+	 *
+	 * SELECT m.member_id, m.member_name FROM members AS m
+	 *
+	 * Since the column name can include up to four segments (host, DB, table, column)
+	 * or also have an alias prefix, we need to do a bit of work to figure this out and
+	 * insert the table prefix (if it exists) in the proper position, and escape only
+	 * the correct identifiers.
+	 *
+	 * @param	string
+	 * @param	bool
+	 * @param	mixed
+	 * @param	bool
+	 * @return	string
+	 */
+	public function protect_identifiers($item, $prefix_single = FALSE, $protect_identifiers = NULL, $field_exists = TRUE)
+	{
+		if ( ! is_bool($protect_identifiers))
+		{
+			$protect_identifiers = $this->_protect_identifiers;
+		}
+
+		if (is_array($item))
+		{
+			$escaped_array = array();
+			foreach ($item as $k => $v)
+			{
+				$escaped_array[$this->protect_identifiers($k)] = $this->protect_identifiers($v, $prefix_single, $protect_identifiers, $field_exists);
+			}
+
+			return $escaped_array;
+		}
+
+		// This is basically a bug fix for queries that use MAX, MIN, etc.
+		// If a parenthesis is found we know that we do not need to
+		// escape the data or add a prefix. There's probably a more graceful
+		// way to deal with this, but I'm not thinking of it -- Rick
+		//
+		// Added exception for single quotes as well, we don't want to alter
+		// literal strings. -- Narf
+		if (strcspn($item, "()'") !== strlen($item))
+		{
+			return $item;
+		}
+
+		// Convert tabs or multiple spaces into single spaces
+		$item = preg_replace('/\s+/', ' ', trim($item));
+
+		// If the item has an alias declaration we remove it and set it aside.
+		// Note: strripos() is used in order to support spaces in table names
+		if ($offset = strripos($item, ' AS '))
+		{
+			$alias = ($protect_identifiers)
+				? substr($item, $offset, 4).$this->escape_identifiers(substr($item, $offset + 4))
+				: substr($item, $offset);
+			$item = substr($item, 0, $offset);
+		}
+		elseif ($offset = strrpos($item, ' '))
+		{
+			$alias = ($protect_identifiers)
+				? ' '.$this->escape_identifiers(substr($item, $offset + 1))
+				: substr($item, $offset);
+			$item = substr($item, 0, $offset);
+		}
+		else
+		{
+			$alias = '';
+		}
+
+		// Break the string apart if it contains periods, then insert the table prefix
+		// in the correct location, assuming the period doesn't indicate that we're dealing
+		// with an alias. While we're at it, we will escape the components
+		if (strpos($item, '.') !== FALSE)
+		{
+			$parts = explode('.', $item);
+
+			// Does the first segment of the exploded item match
+			// one of the aliases previously identified? If so,
+			// we have nothing more to do other than escape the item
+			//
+			// NOTE: The ! empty() condition prevents this method
+			//       from breaking when QB isn't enabled.
+			if ( ! empty($this->qb_aliased_tables) && in_array($parts[0], $this->qb_aliased_tables))
+			{
+				if ($protect_identifiers === TRUE)
+				{
+					foreach ($parts as $key => $val)
+					{
+						if ( ! in_array($val, $this->_reserved_identifiers))
+						{
+							$parts[$key] = $this->escape_identifiers($val);
+						}
+					}
+
+					$item = implode('.', $parts);
+				}
+
+				return $item.$alias;
+			}
+
+			// Is there a table prefix defined in the config file? If not, no need to do anything
+			if ($this->dbprefix !== '')
+			{
+				// We now add the table prefix based on some logic.
+				// Do we have 4 segments (hostname.database.table.column)?
+				// If so, we add the table prefix to the column name in the 3rd segment.
+				if (isset($parts[3]))
+				{
+					$i = 2;
+				}
+				// Do we have 3 segments (database.table.column)?
+				// If so, we add the table prefix to the column name in 2nd position
+				elseif (isset($parts[2]))
+				{
+					$i = 1;
+				}
+				// Do we have 2 segments (table.column)?
+				// If so, we add the table prefix to the column name in 1st segment
+				else
+				{
+					$i = 0;
+				}
+
+				// This flag is set when the supplied $item does not contain a field name.
+				// This can happen when this function is being called from a JOIN.
+				if ($field_exists === FALSE)
+				{
+					$i++;
+				}
+
+				// dbprefix may've already been applied, with or without the identifier escaped
+				$ec = '(?<ec>'.preg_quote(is_array($this->_escape_char) ? $this->_escape_char[0] : $this->_escape_char).')?';
+				isset($ec[0]) && $ec .= '?'; // Just in case someone has disabled escaping by forcing an empty escape character
+
+				// Verify table prefix and replace if necessary
+				if ($this->swap_pre !== '' && preg_match('#^'.$ec.preg_quote($this->swap_pre).'#', $parts[$i]))
+				{
+					$parts[$i] = preg_replace('#^'.$ec.preg_quote($this->swap_pre).'(\S+?)#', '\\1'.$this->dbprefix.'\\2', $parts[$i]);
+				}
+				// We only add the table prefix if it does not already exist
+				else
+				{
+					preg_match('#^'.$ec.preg_quote($this->dbprefix).'#', $parts[$i]) OR $parts[$i] = $this->dbprefix.$parts[$i];
+				}
+
+				// Put the parts back together
+				$item = implode('.', $parts);
+			}
+
+			if ($protect_identifiers === TRUE)
+			{
+				$item = $this->escape_identifiers($item);
+			}
+
+			return $item.$alias;
+		}
+
+		// Is there a table prefix? If not, no need to insert it
+		if ($this->dbprefix !== '')
+		{
+			// Verify table prefix and replace if necessary
+			if ($this->swap_pre !== '' && strpos($item, $this->swap_pre) === 0)
+			{
+				$item = preg_replace('/^'.$this->swap_pre.'(\S+?)/', $this->dbprefix.'\\1', $item);
+			}
+			// Do we prefix an item with no segments?
+			elseif ($prefix_single === TRUE && strpos($item, $this->dbprefix) !== 0)
+			{
+				$item = $this->dbprefix.$item;
+			}
+		}
+
+		if ($protect_identifiers === TRUE && ! in_array($item, $this->_reserved_identifiers))
+		{
+			$item = $this->escape_identifiers($item);
+		}
+
+		return $item.$alias;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Dummy method that allows Query Builder class to be disabled
+	 * and keep count_all() working.
+	 *
+	 * @return	void
+	 */
+	protected function _reset_select()
+	{
+	}
+
+}
diff --git a/system/database/DB_forge.php b/system/database/DB_forge.php
new file mode 100644
index 0000000..64ccde0
--- /dev/null
+++ b/system/database/DB_forge.php
@@ -0,0 +1,1034 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Database Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+abstract class CI_DB_forge {
+
+	/**
+	 * Database object
+	 *
+	 * @var	object
+	 */
+	protected $db;
+
+	/**
+	 * Fields data
+	 *
+	 * @var	array
+	 */
+	public $fields		= array();
+
+	/**
+	 * Keys data
+	 *
+	 * @var	array
+	 */
+	public $keys		= array();
+
+	/**
+	 * Primary Keys data
+	 *
+	 * @var	array
+	 */
+	public $primary_keys	= array();
+
+	/**
+	 * Database character set
+	 *
+	 * @var	string
+	 */
+	public $db_char_set	= '';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * CREATE DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_database	= 'CREATE DATABASE %s';
+
+	/**
+	 * DROP DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_database	= 'DROP DATABASE %s';
+
+	/**
+	 * CREATE TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table	= "%s %s (%s\n)";
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= 'CREATE TABLE IF NOT EXISTS';
+
+	/**
+	 * CREATE TABLE keys flag
+	 *
+	 * Whether table keys are created from within the
+	 * CREATE TABLE statement.
+	 *
+	 * @var	bool
+	 */
+	protected $_create_table_keys	= FALSE;
+
+	/**
+	 * DROP TABLE IF EXISTS statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_table_if	= 'DROP TABLE IF EXISTS';
+
+	/**
+	 * RENAME TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_rename_table	= 'ALTER TABLE %s RENAME TO %s;';
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	bool|array
+	 */
+	protected $_unsigned		= TRUE;
+
+	/**
+	 * NULL value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_null		= '';
+
+	/**
+	 * DEFAULT value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_default		= ' DEFAULT ';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	object	&$db	Database object
+	 * @return	void
+	 */
+	public function __construct(&$db)
+	{
+		$this->db =& $db;
+		log_message('info', 'Database Forge Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Create database
+	 *
+	 * @param	string	$db_name
+	 * @return	bool
+	 */
+	public function create_database($db_name)
+	{
+		if ($this->_create_database === FALSE)
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE;
+		}
+		elseif ( ! $this->db->query(sprintf($this->_create_database, $this->db->escape_identifiers($db_name), $this->db->char_set, $this->db->dbcollat)))
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE;
+		}
+
+		if ( ! empty($this->db->data_cache['db_names']))
+		{
+			$this->db->data_cache['db_names'][] = $db_name;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Drop database
+	 *
+	 * @param	string	$db_name
+	 * @return	bool
+	 */
+	public function drop_database($db_name)
+	{
+		if ($this->_drop_database === FALSE)
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE;
+		}
+		elseif ( ! $this->db->query(sprintf($this->_drop_database, $this->db->escape_identifiers($db_name))))
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE;
+		}
+
+		if ( ! empty($this->db->data_cache['db_names']))
+		{
+			$key = array_search(strtolower($db_name), array_map('strtolower', $this->db->data_cache['db_names']), TRUE);
+			if ($key !== FALSE)
+			{
+				unset($this->db->data_cache['db_names'][$key]);
+			}
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add Key
+	 *
+	 * @param	string	$key
+	 * @param	bool	$primary
+	 * @return	CI_DB_forge
+	 */
+	public function add_key($key, $primary = FALSE)
+	{
+		// DO NOT change this! This condition is only applicable
+		// for PRIMARY keys because you can only have one such,
+		// and therefore all fields you add to it will be included
+		// in the same, composite PRIMARY KEY.
+		//
+		// It's not the same for regular indexes.
+		if ($primary === TRUE && is_array($key))
+		{
+			foreach ($key as $one)
+			{
+				$this->add_key($one, $primary);
+			}
+
+			return $this;
+		}
+
+		if ($primary === TRUE)
+		{
+			$this->primary_keys[] = $key;
+		}
+		else
+		{
+			$this->keys[] = $key;
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Add Field
+	 *
+	 * @param	array	$field
+	 * @return	CI_DB_forge
+	 */
+	public function add_field($field)
+	{
+		if (is_string($field))
+		{
+			if ($field === 'id')
+			{
+				$this->add_field(array(
+					'id' => array(
+						'type' => 'INT',
+						'constraint' => 9,
+						'auto_increment' => TRUE
+					)
+				));
+				$this->add_key('id', TRUE);
+			}
+			else
+			{
+				if (strpos($field, ' ') === FALSE)
+				{
+					show_error('Field information is required for that operation.');
+				}
+
+				$this->fields[] = $field;
+			}
+		}
+
+		if (is_array($field))
+		{
+			$this->fields = array_merge($this->fields, $field);
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Create Table
+	 *
+	 * @param	string	$table		Table name
+	 * @param	bool	$if_not_exists	Whether to add IF NOT EXISTS condition
+	 * @param	array	$attributes	Associative array of table attributes
+	 * @return	bool
+	 */
+	public function create_table($table, $if_not_exists = FALSE, array $attributes = array())
+	{
+		if ($table === '')
+		{
+			show_error('A table name is required for that operation.');
+		}
+		else
+		{
+			$table = $this->db->dbprefix.$table;
+		}
+
+		if (count($this->fields) === 0)
+		{
+			show_error('Field information is required.');
+		}
+
+		$sql = $this->_create_table($table, $if_not_exists, $attributes);
+
+		if (is_bool($sql))
+		{
+			$this->_reset();
+			if ($sql === FALSE)
+			{
+				return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE;
+			}
+		}
+
+		if (($result = $this->db->query($sql)) !== FALSE)
+		{
+			if (isset($this->db->data_cache['table_names']))
+			{
+				$this->db->data_cache['table_names'][] = $table;
+			}
+
+			// Most databases don't support creating indexes from within the CREATE TABLE statement
+			if ( ! empty($this->keys))
+			{
+				for ($i = 0, $sqls = $this->_process_indexes($table), $c = count($sqls); $i < $c; $i++)
+				{
+					$this->db->query($sqls[$i]);
+				}
+			}
+		}
+
+		$this->_reset();
+		return $result;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Create Table
+	 *
+	 * @param	string	$table		Table name
+	 * @param	bool	$if_not_exists	Whether to add 'IF NOT EXISTS' condition
+	 * @param	array	$attributes	Associative array of table attributes
+	 * @return	mixed
+	 */
+	protected function _create_table($table, $if_not_exists, $attributes)
+	{
+		if ($if_not_exists === TRUE && $this->_create_table_if === FALSE)
+		{
+			if ($this->db->table_exists($table))
+			{
+				return TRUE;
+			}
+
+			$if_not_exists = FALSE;
+		}
+
+		$sql = ($if_not_exists)
+			? sprintf($this->_create_table_if, $this->db->escape_identifiers($table))
+			: 'CREATE TABLE';
+
+		$columns = $this->_process_fields(TRUE);
+		for ($i = 0, $c = count($columns); $i < $c; $i++)
+		{
+			$columns[$i] = ($columns[$i]['_literal'] !== FALSE)
+					? "\n\t".$columns[$i]['_literal']
+					: "\n\t".$this->_process_column($columns[$i]);
+		}
+
+		$columns = implode(',', $columns)
+				.$this->_process_primary_keys($table);
+
+		// Are indexes created from within the CREATE TABLE statement? (e.g. in MySQL)
+		if ($this->_create_table_keys === TRUE)
+		{
+			$columns .= $this->_process_indexes($table);
+		}
+
+		// _create_table will usually have the following format: "%s %s (%s\n)"
+		$sql = sprintf($this->_create_table.'%s',
+			$sql,
+			$this->db->escape_identifiers($table),
+			$columns,
+			$this->_create_table_attr($attributes)
+		);
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * CREATE TABLE attributes
+	 *
+	 * @param	array	$attributes	Associative array of table attributes
+	 * @return	string
+	 */
+	protected function _create_table_attr($attributes)
+	{
+		$sql = '';
+
+		foreach (array_keys($attributes) as $key)
+		{
+			if (is_string($key))
+			{
+				$sql .= ' '.strtoupper($key).' '.$attributes[$key];
+			}
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Drop Table
+	 *
+	 * @param	string	$table_name	Table name
+	 * @param	bool	$if_exists	Whether to add an IF EXISTS condition
+	 * @return	bool
+	 */
+	public function drop_table($table_name, $if_exists = FALSE)
+	{
+		if ($table_name === '')
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_table_name_required') : FALSE;
+		}
+
+		if (($query = $this->_drop_table($this->db->dbprefix.$table_name, $if_exists)) === TRUE)
+		{
+			return TRUE;
+		}
+
+		$query = $this->db->query($query);
+
+		// Update table list cache
+		if ($query && ! empty($this->db->data_cache['table_names']))
+		{
+			$key = array_search(strtolower($this->db->dbprefix.$table_name), array_map('strtolower', $this->db->data_cache['table_names']), TRUE);
+			if ($key !== FALSE)
+			{
+				unset($this->db->data_cache['table_names'][$key]);
+			}
+		}
+
+		return $query;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Drop Table
+	 *
+	 * Generates a platform-specific DROP TABLE string
+	 *
+	 * @param	string	$table		Table name
+	 * @param	bool	$if_exists	Whether to add an IF EXISTS condition
+	 * @return	mixed	(Returns a platform-specific DROP table string, or TRUE to indicate there's nothing to do)
+	 */
+	protected function _drop_table($table, $if_exists)
+	{
+		$sql = 'DROP TABLE';
+
+		if ($if_exists)
+		{
+			if ($this->_drop_table_if === FALSE)
+			{
+				if ( ! $this->db->table_exists($table))
+				{
+					return TRUE;
+				}
+			}
+			else
+			{
+				$sql = sprintf($this->_drop_table_if, $this->db->escape_identifiers($table));
+			}
+		}
+
+		return $sql.' '.$this->db->escape_identifiers($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rename Table
+	 *
+	 * @param	string	$table_name	Old table name
+	 * @param	string	$new_table_name	New table name
+	 * @return	bool
+	 */
+	public function rename_table($table_name, $new_table_name)
+	{
+		if ($table_name === '' OR $new_table_name === '')
+		{
+			show_error('A table name is required for that operation.');
+			return FALSE;
+		}
+		elseif ($this->_rename_table === FALSE)
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE;
+		}
+
+		$result = $this->db->query(sprintf($this->_rename_table,
+						$this->db->escape_identifiers($this->db->dbprefix.$table_name),
+						$this->db->escape_identifiers($this->db->dbprefix.$new_table_name))
+					);
+
+		if ($result && ! empty($this->db->data_cache['table_names']))
+		{
+			$key = array_search(strtolower($this->db->dbprefix.$table_name), array_map('strtolower', $this->db->data_cache['table_names']), TRUE);
+			if ($key !== FALSE)
+			{
+				$this->db->data_cache['table_names'][$key] = $this->db->dbprefix.$new_table_name;
+			}
+		}
+
+		return $result;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Column Add
+	 *
+	 * @todo	Remove deprecated $_after option in 3.1+
+	 * @param	string	$table	Table name
+	 * @param	array	$field	Column definition
+	 * @param	string	$_after	Column for AFTER clause (deprecated)
+	 * @return	bool
+	 */
+	public function add_column($table, $field, $_after = NULL)
+	{
+		// Work-around for literal column definitions
+		is_array($field) OR $field = array($field);
+
+		foreach (array_keys($field) as $k)
+		{
+			// Backwards-compatibility work-around for MySQL/CUBRID AFTER clause (remove in 3.1+)
+			if ($_after !== NULL && is_array($field[$k]) && ! isset($field[$k]['after']))
+			{
+				$field[$k]['after'] = $_after;
+			}
+
+			$this->add_field(array($k => $field[$k]));
+		}
+
+		$sqls = $this->_alter_table('ADD', $this->db->dbprefix.$table, $this->_process_fields());
+		$this->_reset();
+		if ($sqls === FALSE)
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE;
+		}
+
+		for ($i = 0, $c = count($sqls); $i < $c; $i++)
+		{
+			if ($this->db->query($sqls[$i]) === FALSE)
+			{
+				return FALSE;
+			}
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Column Drop
+	 *
+	 * @param	string	$table		Table name
+	 * @param	string	$column_name	Column name
+	 * @return	bool
+	 */
+	public function drop_column($table, $column_name)
+	{
+		$sql = $this->_alter_table('DROP', $this->db->dbprefix.$table, $column_name);
+		if ($sql === FALSE)
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE;
+		}
+
+		return $this->db->query($sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Column Modify
+	 *
+	 * @param	string	$table	Table name
+	 * @param	string	$field	Column definition
+	 * @return	bool
+	 */
+	public function modify_column($table, $field)
+	{
+		// Work-around for literal column definitions
+		is_array($field) OR $field = array($field);
+
+		foreach (array_keys($field) as $k)
+		{
+			$this->add_field(array($k => $field[$k]));
+		}
+
+		if (count($this->fields) === 0)
+		{
+			show_error('Field information is required.');
+		}
+
+		$sqls = $this->_alter_table('CHANGE', $this->db->dbprefix.$table, $this->_process_fields());
+		$this->_reset();
+		if ($sqls === FALSE)
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE;
+		}
+
+		for ($i = 0, $c = count($sqls); $i < $c; $i++)
+		{
+			if ($this->db->query($sqls[$i]) === FALSE)
+			{
+				return FALSE;
+			}
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' ';
+
+		// DROP has everything it needs now.
+		if ($alter_type === 'DROP')
+		{
+			return $sql.'DROP COLUMN '.$this->db->escape_identifiers($field);
+		}
+
+		$sql .= ($alter_type === 'ADD')
+			? 'ADD '
+			: $alter_type.' COLUMN ';
+
+		$sqls = array();
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			$sqls[] = $sql
+				.($field[$i]['_literal'] !== FALSE ? $field[$i]['_literal'] : $this->_process_column($field[$i]));
+		}
+
+		return $sqls;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process fields
+	 *
+	 * @param	bool	$create_table
+	 * @return	array
+	 */
+	protected function _process_fields($create_table = FALSE)
+	{
+		$fields = array();
+
+		foreach ($this->fields as $key => $attributes)
+		{
+			if (is_int($key) && ! is_array($attributes))
+			{
+				$fields[] = array('_literal' => $attributes);
+				continue;
+			}
+
+			$attributes = array_change_key_case($attributes, CASE_UPPER);
+
+			if ($create_table === TRUE && empty($attributes['TYPE']))
+			{
+				continue;
+			}
+
+			isset($attributes['TYPE']) && $this->_attr_type($attributes);
+
+			$field = array(
+				'name'			=> $key,
+				'new_name'		=> isset($attributes['NAME']) ? $attributes['NAME'] : NULL,
+				'type'			=> isset($attributes['TYPE']) ? $attributes['TYPE'] : NULL,
+				'length'		=> '',
+				'unsigned'		=> '',
+				'null'			=> NULL,
+				'unique'		=> '',
+				'default'		=> '',
+				'auto_increment'	=> '',
+				'_literal'		=> FALSE
+			);
+
+			isset($attributes['TYPE']) && $this->_attr_unsigned($attributes, $field);
+
+			if ($create_table === FALSE)
+			{
+				if (isset($attributes['AFTER']))
+				{
+					$field['after'] = $attributes['AFTER'];
+				}
+				elseif (isset($attributes['FIRST']))
+				{
+					$field['first'] = (bool) $attributes['FIRST'];
+				}
+			}
+
+			$this->_attr_default($attributes, $field);
+
+			if (isset($attributes['NULL']))
+			{
+				if ($attributes['NULL'] === TRUE)
+				{
+					$field['null'] = empty($this->_null) ? '' : ' '.$this->_null;
+				}
+				else
+				{
+					$field['null'] = ' NOT NULL';
+				}
+			}
+			elseif ($create_table === TRUE)
+			{
+				$field['null'] = ' NOT NULL';
+			}
+
+			$this->_attr_auto_increment($attributes, $field);
+			$this->_attr_unique($attributes, $field);
+
+			if (isset($attributes['COMMENT']))
+			{
+				$field['comment'] = $this->db->escape($attributes['COMMENT']);
+			}
+
+			if (isset($attributes['TYPE']) && ! empty($attributes['CONSTRAINT']))
+			{
+				switch (strtoupper($attributes['TYPE']))
+				{
+					case 'ENUM':
+					case 'SET':
+						$attributes['CONSTRAINT'] = $this->db->escape($attributes['CONSTRAINT']);
+					default:
+						$field['length'] = is_array($attributes['CONSTRAINT'])
+							? '('.implode(',', $attributes['CONSTRAINT']).')'
+							: '('.$attributes['CONSTRAINT'].')';
+						break;
+				}
+			}
+
+			$fields[] = $field;
+		}
+
+		return $fields;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process column
+	 *
+	 * @param	array	$field
+	 * @return	string
+	 */
+	protected function _process_column($field)
+	{
+		return $this->db->escape_identifiers($field['name'])
+			.' '.$field['type'].$field['length']
+			.$field['unsigned']
+			.$field['default']
+			.$field['null']
+			.$field['auto_increment']
+			.$field['unique'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		// Usually overridden by drivers
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute UNSIGNED
+	 *
+	 * Depending on the _unsigned property value:
+	 *
+	 *	- TRUE will always set $field['unsigned'] to 'UNSIGNED'
+	 *	- FALSE will always set $field['unsigned'] to ''
+	 *	- array(TYPE) will set $field['unsigned'] to 'UNSIGNED',
+	 *		if $attributes['TYPE'] is found in the array
+	 *	- array(TYPE => UTYPE) will change $field['type'],
+	 *		from TYPE to UTYPE in case of a match
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_unsigned(&$attributes, &$field)
+	{
+		if (empty($attributes['UNSIGNED']) OR $attributes['UNSIGNED'] !== TRUE)
+		{
+			return;
+		}
+
+		// Reset the attribute in order to avoid issues if we do type conversion
+		$attributes['UNSIGNED'] = FALSE;
+
+		if (is_array($this->_unsigned))
+		{
+			foreach (array_keys($this->_unsigned) as $key)
+			{
+				if (is_int($key) && strcasecmp($attributes['TYPE'], $this->_unsigned[$key]) === 0)
+				{
+					$field['unsigned'] = ' UNSIGNED';
+					return;
+				}
+				elseif (is_string($key) && strcasecmp($attributes['TYPE'], $key) === 0)
+				{
+					$field['type'] = $key;
+					return;
+				}
+			}
+
+			return;
+		}
+
+		$field['unsigned'] = ($this->_unsigned === TRUE) ? ' UNSIGNED' : '';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute DEFAULT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_default(&$attributes, &$field)
+	{
+		if ($this->_default === FALSE)
+		{
+			return;
+		}
+
+		if (array_key_exists('DEFAULT', $attributes))
+		{
+			if ($attributes['DEFAULT'] === NULL)
+			{
+				$field['default'] = empty($this->_null) ? '' : $this->_default.$this->_null;
+
+				// Override the NULL attribute if that's our default
+				$attributes['NULL'] = TRUE;
+				$field['null'] = empty($this->_null) ? '' : ' '.$this->_null;
+			}
+			else
+			{
+				$field['default'] = $this->_default.$this->db->escape($attributes['DEFAULT']);
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute UNIQUE
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_unique(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === TRUE)
+		{
+			$field['unique'] = ' UNIQUE';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE)
+		{
+			$field['auto_increment'] = ' AUTO_INCREMENT';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process primary keys
+	 *
+	 * @param	string	$table	Table name
+	 * @return	string
+	 */
+	protected function _process_primary_keys($table)
+	{
+		$sql = '';
+
+		for ($i = 0, $c = count($this->primary_keys); $i < $c; $i++)
+		{
+			if ( ! isset($this->fields[$this->primary_keys[$i]]))
+			{
+				unset($this->primary_keys[$i]);
+			}
+		}
+
+		if (count($this->primary_keys) > 0)
+		{
+			$sql .= ",\n\tCONSTRAINT ".$this->db->escape_identifiers('pk_'.$table)
+				.' PRIMARY KEY('.implode(', ', $this->db->escape_identifiers($this->primary_keys)).')';
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process indexes
+	 *
+	 * @param	string	$table	Table name
+	 * @return	string[] list of SQL statements
+	 */
+	protected function _process_indexes($table)
+	{
+		$sqls = array();
+
+		for ($i = 0, $c = count($this->keys); $i < $c; $i++)
+		{
+			if (is_array($this->keys[$i]))
+			{
+				for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++)
+				{
+					if ( ! isset($this->fields[$this->keys[$i][$i2]]))
+					{
+						unset($this->keys[$i][$i2]);
+						continue;
+					}
+				}
+			}
+			elseif ( ! isset($this->fields[$this->keys[$i]]))
+			{
+				unset($this->keys[$i]);
+				continue;
+			}
+
+			is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]);
+
+			$sqls[] = 'CREATE INDEX '.$this->db->escape_identifiers($table.'_'.implode('_', $this->keys[$i]))
+				.' ON '.$this->db->escape_identifiers($table)
+				.' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).');';
+		}
+
+		return $sqls;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Reset
+	 *
+	 * Resets table creation vars
+	 *
+	 * @return	void
+	 */
+	protected function _reset()
+	{
+		$this->fields = $this->keys = $this->primary_keys = array();
+	}
+
+}
diff --git a/system/database/DB_query_builder.php b/system/database/DB_query_builder.php
new file mode 100644
index 0000000..9331084
--- /dev/null
+++ b/system/database/DB_query_builder.php
@@ -0,0 +1,2809 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Query Builder Class
+ *
+ * This is the platform-independent base Query Builder implementation class.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+
+abstract class CI_DB_query_builder extends CI_DB_driver {
+
+	/**
+	 * Return DELETE SQL flag
+	 *
+	 * @var	bool
+	 */
+	protected $return_delete_sql		= FALSE;
+
+	/**
+	 * Reset DELETE data flag
+	 *
+	 * @var	bool
+	 */
+	protected $reset_delete_data		= FALSE;
+
+	/**
+	 * QB SELECT data
+	 *
+	 * @var	array
+	 */
+	protected $qb_select			= array();
+
+	/**
+	 * QB DISTINCT flag
+	 *
+	 * @var	bool
+	 */
+	protected $qb_distinct			= FALSE;
+
+	/**
+	 * QB FROM data
+	 *
+	 * @var	array
+	 */
+	protected $qb_from			= array();
+
+	/**
+	 * QB JOIN data
+	 *
+	 * @var	array
+	 */
+	protected $qb_join			= array();
+
+	/**
+	 * QB WHERE data
+	 *
+	 * @var	array
+	 */
+	protected $qb_where			= array();
+
+	/**
+	 * QB GROUP BY data
+	 *
+	 * @var	array
+	 */
+	protected $qb_groupby			= array();
+
+	/**
+	 * QB HAVING data
+	 *
+	 * @var	array
+	 */
+	protected $qb_having			= array();
+
+	/**
+	 * QB keys
+	 *
+	 * @var	array
+	 */
+	protected $qb_keys			= array();
+
+	/**
+	 * QB LIMIT data
+	 *
+	 * @var	int
+	 */
+	protected $qb_limit			= FALSE;
+
+	/**
+	 * QB OFFSET data
+	 *
+	 * @var	int
+	 */
+	protected $qb_offset			= FALSE;
+
+	/**
+	 * QB ORDER BY data
+	 *
+	 * @var	array
+	 */
+	protected $qb_orderby			= array();
+
+	/**
+	 * QB data sets
+	 *
+	 * @var	array
+	 */
+	protected $qb_set			= array();
+
+	/**
+	 * QB data set for update_batch()
+	 *
+	 * @var	array
+	 */
+	protected $qb_set_ub			= array();
+
+	/**
+	 * QB aliased tables list
+	 *
+	 * @var	array
+	 */
+	protected $qb_aliased_tables		= array();
+
+	/**
+	 * QB WHERE group started flag
+	 *
+	 * @var	bool
+	 */
+	protected $qb_where_group_started	= FALSE;
+
+	/**
+	 * QB WHERE group count
+	 *
+	 * @var	int
+	 */
+	protected $qb_where_group_count		= 0;
+
+	// Query Builder Caching variables
+
+	/**
+	 * QB Caching flag
+	 *
+	 * @var	bool
+	 */
+	protected $qb_caching				= FALSE;
+
+	/**
+	 * QB Cache exists list
+	 *
+	 * @var	array
+	 */
+	protected $qb_cache_exists			= array();
+
+	/**
+	 * QB Cache SELECT data
+	 *
+	 * @var	array
+	 */
+	protected $qb_cache_select			= array();
+
+	/**
+	 * QB Cache FROM data
+	 *
+	 * @var	array
+	 */
+	protected $qb_cache_from			= array();
+
+	/**
+	 * QB Cache JOIN data
+	 *
+	 * @var	array
+	 */
+	protected $qb_cache_join			= array();
+
+	/**
+	 * QB Cache aliased tables list
+	 *
+	 * @var	array
+	 */
+	protected $qb_cache_aliased_tables			= array();
+
+	/**
+	 * QB Cache WHERE data
+	 *
+	 * @var	array
+	 */
+	protected $qb_cache_where			= array();
+
+	/**
+	 * QB Cache GROUP BY data
+	 *
+	 * @var	array
+	 */
+	protected $qb_cache_groupby			= array();
+
+	/**
+	 * QB Cache HAVING data
+	 *
+	 * @var	array
+	 */
+	protected $qb_cache_having			= array();
+
+	/**
+	 * QB Cache ORDER BY data
+	 *
+	 * @var	array
+	 */
+	protected $qb_cache_orderby			= array();
+
+	/**
+	 * QB Cache data sets
+	 *
+	 * @var	array
+	 */
+	protected $qb_cache_set				= array();
+
+	/**
+	 * QB No Escape data
+	 *
+	 * @var	array
+	 */
+	protected $qb_no_escape 			= array();
+
+	/**
+	 * QB Cache No Escape data
+	 *
+	 * @var	array
+	 */
+	protected $qb_cache_no_escape			= array();
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Select
+	 *
+	 * Generates the SELECT portion of the query
+	 *
+	 * @param	string
+	 * @param	mixed
+	 * @return	CI_DB_query_builder
+	 */
+	public function select($select = '*', $escape = NULL)
+	{
+		if (is_string($select))
+		{
+			$select = explode(',', $select);
+		}
+
+		// If the escape value was not set, we will base it on the global setting
+		is_bool($escape) OR $escape = $this->_protect_identifiers;
+
+		foreach ($select as $val)
+		{
+			$val = trim($val);
+
+			if ($val !== '')
+			{
+				$this->qb_select[] = $val;
+				$this->qb_no_escape[] = $escape;
+
+				if ($this->qb_caching === TRUE)
+				{
+					$this->qb_cache_select[] = $val;
+					$this->qb_cache_exists[] = 'select';
+					$this->qb_cache_no_escape[] = $escape;
+				}
+			}
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Select Max
+	 *
+	 * Generates a SELECT MAX(field) portion of a query
+	 *
+	 * @param	string	the field
+	 * @param	string	an alias
+	 * @return	CI_DB_query_builder
+	 */
+	public function select_max($select = '', $alias = '')
+	{
+		return $this->_max_min_avg_sum($select, $alias, 'MAX');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Select Min
+	 *
+	 * Generates a SELECT MIN(field) portion of a query
+	 *
+	 * @param	string	the field
+	 * @param	string	an alias
+	 * @return	CI_DB_query_builder
+	 */
+	public function select_min($select = '', $alias = '')
+	{
+		return $this->_max_min_avg_sum($select, $alias, 'MIN');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Select Average
+	 *
+	 * Generates a SELECT AVG(field) portion of a query
+	 *
+	 * @param	string	the field
+	 * @param	string	an alias
+	 * @return	CI_DB_query_builder
+	 */
+	public function select_avg($select = '', $alias = '')
+	{
+		return $this->_max_min_avg_sum($select, $alias, 'AVG');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Select Sum
+	 *
+	 * Generates a SELECT SUM(field) portion of a query
+	 *
+	 * @param	string	the field
+	 * @param	string	an alias
+	 * @return	CI_DB_query_builder
+	 */
+	public function select_sum($select = '', $alias = '')
+	{
+		return $this->_max_min_avg_sum($select, $alias, 'SUM');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * SELECT [MAX|MIN|AVG|SUM]()
+	 *
+	 * @used-by	select_max()
+	 * @used-by	select_min()
+	 * @used-by	select_avg()
+	 * @used-by	select_sum()
+	 *
+	 * @param	string	$select	Field name
+	 * @param	string	$alias
+	 * @param	string	$type
+	 * @return	CI_DB_query_builder
+	 */
+	protected function _max_min_avg_sum($select = '', $alias = '', $type = 'MAX')
+	{
+		if ( ! is_string($select) OR $select === '')
+		{
+			$this->display_error('db_invalid_query');
+		}
+
+		$type = strtoupper($type);
+
+		if ( ! in_array($type, array('MAX', 'MIN', 'AVG', 'SUM')))
+		{
+			show_error('Invalid function type: '.$type);
+		}
+
+		if ($alias === '')
+		{
+			$alias = $this->_create_alias_from_table(trim($select));
+		}
+
+		$sql = $type.'('.$this->protect_identifiers(trim($select)).') AS '.$this->escape_identifiers(trim($alias));
+
+		$this->qb_select[] = $sql;
+		$this->qb_no_escape[] = NULL;
+
+		if ($this->qb_caching === TRUE)
+		{
+			$this->qb_cache_select[] = $sql;
+			$this->qb_cache_exists[] = 'select';
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Determines the alias name based on the table
+	 *
+	 * @param	string	$item
+	 * @return	string
+	 */
+	protected function _create_alias_from_table($item)
+	{
+		if (strpos($item, '.') !== FALSE)
+		{
+			$item = explode('.', $item);
+			return end($item);
+		}
+
+		return $item;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * DISTINCT
+	 *
+	 * Sets a flag which tells the query string compiler to add DISTINCT
+	 *
+	 * @param	bool	$val
+	 * @return	CI_DB_query_builder
+	 */
+	public function distinct($val = TRUE)
+	{
+		$this->qb_distinct = is_bool($val) ? $val : TRUE;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * From
+	 *
+	 * Generates the FROM portion of the query
+	 *
+	 * @param	mixed	$from	can be a string or array
+	 * @return	CI_DB_query_builder
+	 */
+	public function from($from)
+	{
+		foreach ((array) $from as $val)
+		{
+			if (strpos($val, ',') !== FALSE)
+			{
+				foreach (explode(',', $val) as $v)
+				{
+					$v = trim($v);
+					$this->_track_aliases($v);
+
+					$this->qb_from[] = $v = $this->protect_identifiers($v, TRUE, NULL, FALSE);
+
+					if ($this->qb_caching === TRUE)
+					{
+						$this->qb_cache_from[] = $v;
+						$this->qb_cache_exists[] = 'from';
+					}
+				}
+			}
+			else
+			{
+				$val = trim($val);
+
+				// Extract any aliases that might exist. We use this information
+				// in the protect_identifiers to know whether to add a table prefix
+				$this->_track_aliases($val);
+
+				$this->qb_from[] = $val = $this->protect_identifiers($val, TRUE, NULL, FALSE);
+
+				if ($this->qb_caching === TRUE)
+				{
+					$this->qb_cache_from[] = $val;
+					$this->qb_cache_exists[] = 'from';
+				}
+			}
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * JOIN
+	 *
+	 * Generates the JOIN portion of the query
+	 *
+	 * @param	string
+	 * @param	string	the join condition
+	 * @param	string	the type of join
+	 * @param	string	whether not to try to escape identifiers
+	 * @return	CI_DB_query_builder
+	 */
+	public function join($table, $cond, $type = '', $escape = NULL)
+	{
+		if ($type !== '')
+		{
+			$type = strtoupper(trim($type));
+
+			if ( ! in_array($type, array('LEFT', 'RIGHT', 'OUTER', 'INNER', 'LEFT OUTER', 'RIGHT OUTER', 'FULL OUTER', 'FULL'), TRUE))
+			{
+				$type = '';
+			}
+			else
+			{
+				$type .= ' ';
+			}
+		}
+
+		// Extract any aliases that might exist. We use this information
+		// in the protect_identifiers to know whether to add a table prefix
+		$this->_track_aliases($table);
+
+		is_bool($escape) OR $escape = $this->_protect_identifiers;
+
+		if ( ! $this->_has_operator($cond))
+		{
+			$cond = ' USING ('.($escape ? $this->escape_identifiers($cond) : $cond).')';
+		}
+		elseif ($escape === FALSE)
+		{
+			$cond = ' ON '.$cond;
+		}
+		else
+		{
+			// Split multiple conditions
+			if (preg_match_all('/\sAND\s|\sOR\s/i', $cond, $joints, PREG_OFFSET_CAPTURE))
+			{
+				$conditions = array();
+				$joints = $joints[0];
+				array_unshift($joints, array('', 0));
+
+				for ($i = count($joints) - 1, $pos = strlen($cond); $i >= 0; $i--)
+				{
+					$joints[$i][1] += strlen($joints[$i][0]); // offset
+					$conditions[$i] = substr($cond, $joints[$i][1], $pos - $joints[$i][1]);
+					$pos = $joints[$i][1] - strlen($joints[$i][0]);
+					$joints[$i] = $joints[$i][0];
+				}
+			}
+			else
+			{
+				$conditions = array($cond);
+				$joints = array('');
+			}
+
+			$cond = ' ON ';
+			for ($i = 0, $c = count($conditions); $i < $c; $i++)
+			{
+				$operator = $this->_get_operator($conditions[$i]);
+				$cond .= $joints[$i];
+				$cond .= preg_match("/(\(*)?([\[\]\w\.'-]+)".preg_quote($operator)."(.*)/i", $conditions[$i], $match)
+					? $match[1].$this->protect_identifiers($match[2]).$operator.$this->protect_identifiers($match[3])
+					: $conditions[$i];
+			}
+		}
+
+		// Do we want to escape the table name?
+		if ($escape === TRUE)
+		{
+			$table = $this->protect_identifiers($table, TRUE, NULL, FALSE);
+		}
+
+		// Assemble the JOIN statement
+		$this->qb_join[] = $join = $type.'JOIN '.$table.$cond;
+
+		if ($this->qb_caching === TRUE)
+		{
+			$this->qb_cache_join[] = $join;
+			$this->qb_cache_exists[] = 'join';
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * WHERE
+	 *
+	 * Generates the WHERE portion of the query.
+	 * Separates multiple calls with 'AND'.
+	 *
+	 * @param	mixed
+	 * @param	mixed
+	 * @param	bool
+	 * @return	CI_DB_query_builder
+	 */
+	public function where($key, $value = NULL, $escape = NULL)
+	{
+		return $this->_wh('qb_where', $key, $value, 'AND ', $escape);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * OR WHERE
+	 *
+	 * Generates the WHERE portion of the query.
+	 * Separates multiple calls with 'OR'.
+	 *
+	 * @param	mixed
+	 * @param	mixed
+	 * @param	bool
+	 * @return	CI_DB_query_builder
+	 */
+	public function or_where($key, $value = NULL, $escape = NULL)
+	{
+		return $this->_wh('qb_where', $key, $value, 'OR ', $escape);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * WHERE, HAVING
+	 *
+	 * @used-by	where()
+	 * @used-by	or_where()
+	 * @used-by	having()
+	 * @used-by	or_having()
+	 *
+	 * @param	string	$qb_key	'qb_where' or 'qb_having'
+	 * @param	mixed	$key
+	 * @param	mixed	$value
+	 * @param	string	$type
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	protected function _wh($qb_key, $key, $value = NULL, $type = 'AND ', $escape = NULL)
+	{
+		$qb_cache_key = ($qb_key === 'qb_having') ? 'qb_cache_having' : 'qb_cache_where';
+
+		if ( ! is_array($key))
+		{
+			$key = array($key => $value);
+		}
+
+		// If the escape value was not set will base it on the global setting
+		is_bool($escape) OR $escape = $this->_protect_identifiers;
+
+		foreach ($key as $k => $v)
+		{
+			$prefix = (count($this->$qb_key) === 0 && count($this->$qb_cache_key) === 0)
+				? $this->_group_get_type('')
+				: $this->_group_get_type($type);
+
+			if ($v !== NULL)
+			{
+				if ($escape === TRUE)
+				{
+					$v = $this->escape($v);
+				}
+
+				if ( ! $this->_has_operator($k))
+				{
+					$k .= ' = ';
+				}
+			}
+			elseif ( ! $this->_has_operator($k))
+			{
+				// value appears not to have been set, assign the test to IS NULL
+				$k .= ' IS NULL';
+			}
+			elseif (preg_match('/\s*(!?=|<>|\sIS(?:\s+NOT)?\s)\s*$/i', $k, $match, PREG_OFFSET_CAPTURE))
+			{
+				$k = substr($k, 0, $match[0][1]).($match[1][0] === '=' ? ' IS NULL' : ' IS NOT NULL');
+			}
+
+			${$qb_key} = array('condition' => $prefix.$k, 'value' => $v, 'escape' => $escape);
+			$this->{$qb_key}[] = ${$qb_key};
+			if ($this->qb_caching === TRUE)
+			{
+				$this->{$qb_cache_key}[] = ${$qb_key};
+				$this->qb_cache_exists[] = substr($qb_key, 3);
+			}
+
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * WHERE IN
+	 *
+	 * Generates a WHERE field IN('item', 'item') SQL query,
+	 * joined with 'AND' if appropriate.
+	 *
+	 * @param	string	$key	The field to search
+	 * @param	array	$values	The values searched on
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	public function where_in($key = NULL, $values = NULL, $escape = NULL)
+	{
+		return $this->_where_in($key, $values, FALSE, 'AND ', $escape);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * OR WHERE IN
+	 *
+	 * Generates a WHERE field IN('item', 'item') SQL query,
+	 * joined with 'OR' if appropriate.
+	 *
+	 * @param	string	$key	The field to search
+	 * @param	array	$values	The values searched on
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	public function or_where_in($key = NULL, $values = NULL, $escape = NULL)
+	{
+		return $this->_where_in($key, $values, FALSE, 'OR ', $escape);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * WHERE NOT IN
+	 *
+	 * Generates a WHERE field NOT IN('item', 'item') SQL query,
+	 * joined with 'AND' if appropriate.
+	 *
+	 * @param	string	$key	The field to search
+	 * @param	array	$values	The values searched on
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	public function where_not_in($key = NULL, $values = NULL, $escape = NULL)
+	{
+		return $this->_where_in($key, $values, TRUE, 'AND ', $escape);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * OR WHERE NOT IN
+	 *
+	 * Generates a WHERE field NOT IN('item', 'item') SQL query,
+	 * joined with 'OR' if appropriate.
+	 *
+	 * @param	string	$key	The field to search
+	 * @param	array	$values	The values searched on
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	public function or_where_not_in($key = NULL, $values = NULL, $escape = NULL)
+	{
+		return $this->_where_in($key, $values, TRUE, 'OR ', $escape);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Internal WHERE IN
+	 *
+	 * @used-by	where_in()
+	 * @used-by	or_where_in()
+	 * @used-by	where_not_in()
+	 * @used-by	or_where_not_in()
+	 *
+	 * @param	string	$key	The field to search
+	 * @param	array	$values	The values searched on
+	 * @param	bool	$not	If the statement would be IN or NOT IN
+	 * @param	string	$type
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	protected function _where_in($key = NULL, $values = NULL, $not = FALSE, $type = 'AND ', $escape = NULL)
+	{
+		if ($key === NULL OR $values === NULL)
+		{
+			return $this;
+		}
+
+		if ( ! is_array($values))
+		{
+			$values = array($values);
+		}
+
+		is_bool($escape) OR $escape = $this->_protect_identifiers;
+
+		$not = ($not) ? ' NOT' : '';
+
+		if ($escape === TRUE)
+		{
+			$where_in = array();
+			foreach ($values as $value)
+			{
+				$where_in[] = $this->escape($value);
+			}
+		}
+		else
+		{
+			$where_in = array_values($values);
+		}
+
+		$prefix = (count($this->qb_where) === 0 && count($this->qb_cache_where) === 0)
+			? $this->_group_get_type('')
+			: $this->_group_get_type($type);
+
+		$where_in = array(
+			'condition' => $prefix.$key.$not.' IN('.implode(', ', $where_in).')',
+			'value' => NULL,
+			'escape' => $escape
+		);
+
+		$this->qb_where[] = $where_in;
+		if ($this->qb_caching === TRUE)
+		{
+			$this->qb_cache_where[] = $where_in;
+			$this->qb_cache_exists[] = 'where';
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIKE
+	 *
+	 * Generates a %LIKE% portion of the query.
+	 * Separates multiple calls with 'AND'.
+	 *
+	 * @param	mixed	$field
+	 * @param	string	$match
+	 * @param	string	$side
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	public function like($field, $match = '', $side = 'both', $escape = NULL)
+	{
+		return $this->_like($field, $match, 'AND ', $side, '', $escape);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * NOT LIKE
+	 *
+	 * Generates a NOT LIKE portion of the query.
+	 * Separates multiple calls with 'AND'.
+	 *
+	 * @param	mixed	$field
+	 * @param	string	$match
+	 * @param	string	$side
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	public function not_like($field, $match = '', $side = 'both', $escape = NULL)
+	{
+		return $this->_like($field, $match, 'AND ', $side, 'NOT', $escape);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * OR LIKE
+	 *
+	 * Generates a %LIKE% portion of the query.
+	 * Separates multiple calls with 'OR'.
+	 *
+	 * @param	mixed	$field
+	 * @param	string	$match
+	 * @param	string	$side
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	public function or_like($field, $match = '', $side = 'both', $escape = NULL)
+	{
+		return $this->_like($field, $match, 'OR ', $side, '', $escape);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * OR NOT LIKE
+	 *
+	 * Generates a NOT LIKE portion of the query.
+	 * Separates multiple calls with 'OR'.
+	 *
+	 * @param	mixed	$field
+	 * @param	string	$match
+	 * @param	string	$side
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	public function or_not_like($field, $match = '', $side = 'both', $escape = NULL)
+	{
+		return $this->_like($field, $match, 'OR ', $side, 'NOT', $escape);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Internal LIKE
+	 *
+	 * @used-by	like()
+	 * @used-by	or_like()
+	 * @used-by	not_like()
+	 * @used-by	or_not_like()
+	 *
+	 * @param	mixed	$field
+	 * @param	string	$match
+	 * @param	string	$type
+	 * @param	string	$side
+	 * @param	string	$not
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	protected function _like($field, $match = '', $type = 'AND ', $side = 'both', $not = '', $escape = NULL)
+	{
+		if ( ! is_array($field))
+		{
+			$field = array($field => $match);
+		}
+
+		is_bool($escape) OR $escape = $this->_protect_identifiers;
+		// lowercase $side in case somebody writes e.g. 'BEFORE' instead of 'before' (doh)
+		$side = strtolower($side);
+
+		foreach ($field as $k => $v)
+		{
+			$prefix = (count($this->qb_where) === 0 && count($this->qb_cache_where) === 0)
+				? $this->_group_get_type('') : $this->_group_get_type($type);
+
+			if ($escape === TRUE)
+			{
+				$v = $this->escape_like_str($v);
+			}
+
+			switch ($side)
+			{
+				case 'none':
+					$v = "'{$v}'";
+					break;
+				case 'before':
+					$v = "'%{$v}'";
+					break;
+				case 'after':
+					$v = "'{$v}%'";
+					break;
+				case 'both':
+				default:
+					$v = "'%{$v}%'";
+					break;
+			}
+
+			// some platforms require an escape sequence definition for LIKE wildcards
+			if ($escape === TRUE && $this->_like_escape_str !== '')
+			{
+				$v .= sprintf($this->_like_escape_str, $this->_like_escape_chr);
+			}
+
+			$qb_where = array('condition' => "{$prefix} {$k} {$not} LIKE {$v}", 'value' => NULL, 'escape' => $escape);
+			$this->qb_where[] = $qb_where;
+			if ($this->qb_caching === TRUE)
+			{
+				$this->qb_cache_where[] = $qb_where;
+				$this->qb_cache_exists[] = 'where';
+			}
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Starts a query group.
+	 *
+	 * @param	string	$not	(Internal use only)
+	 * @param	string	$type	(Internal use only)
+	 * @return	CI_DB_query_builder
+	 */
+	public function group_start($not = '', $type = 'AND ')
+	{
+		$type = $this->_group_get_type($type);
+
+		$this->qb_where_group_started = TRUE;
+		$prefix = (count($this->qb_where) === 0 && count($this->qb_cache_where) === 0) ? '' : $type;
+		$where = array(
+			'condition' => $prefix.$not.str_repeat(' ', ++$this->qb_where_group_count).' (',
+			'value' => NULL,
+			'escape' => FALSE
+		);
+
+		$this->qb_where[] = $where;
+		if ($this->qb_caching)
+		{
+			$this->qb_cache_where[] = $where;
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Starts a query group, but ORs the group
+	 *
+	 * @return	CI_DB_query_builder
+	 */
+	public function or_group_start()
+	{
+		return $this->group_start('', 'OR ');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Starts a query group, but NOTs the group
+	 *
+	 * @return	CI_DB_query_builder
+	 */
+	public function not_group_start()
+	{
+		return $this->group_start('NOT ', 'AND ');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Starts a query group, but OR NOTs the group
+	 *
+	 * @return	CI_DB_query_builder
+	 */
+	public function or_not_group_start()
+	{
+		return $this->group_start('NOT ', 'OR ');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Ends a query group
+	 *
+	 * @return	CI_DB_query_builder
+	 */
+	public function group_end()
+	{
+		$this->qb_where_group_started = FALSE;
+		$where = array(
+			'condition' => str_repeat(' ', $this->qb_where_group_count--).')',
+			'value' => NULL,
+			'escape' => FALSE
+		);
+
+		$this->qb_where[] = $where;
+		if ($this->qb_caching)
+		{
+			$this->qb_cache_where[] = $where;
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Group_get_type
+	 *
+	 * @used-by	group_start()
+	 * @used-by	_like()
+	 * @used-by	_wh()
+	 * @used-by	_where_in()
+	 *
+	 * @param	string	$type
+	 * @return	string
+	 */
+	protected function _group_get_type($type)
+	{
+		if ($this->qb_where_group_started)
+		{
+			$type = '';
+			$this->qb_where_group_started = FALSE;
+		}
+
+		return $type;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * GROUP BY
+	 *
+	 * @param	mixed	$by
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	public function group_by($by, $escape = NULL)
+	{
+		is_bool($escape) OR $escape = $this->_protect_identifiers;
+
+		if (is_string($by))
+		{
+			$by = ($escape === TRUE)
+				? explode(',', $by)
+				: array($by);
+		}
+
+		foreach ($by as $val)
+		{
+			$val = trim($val);
+
+			if ($val !== '')
+			{
+				$val = array('field' => $val, 'escape' => $escape);
+
+				$this->qb_groupby[] = $val;
+				if ($this->qb_caching === TRUE)
+				{
+					$this->qb_cache_groupby[] = $val;
+					$this->qb_cache_exists[] = 'groupby';
+				}
+			}
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * HAVING
+	 *
+	 * Separates multiple calls with 'AND'.
+	 *
+	 * @param	string	$key
+	 * @param	string	$value
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	public function having($key, $value = NULL, $escape = NULL)
+	{
+		return $this->_wh('qb_having', $key, $value, 'AND ', $escape);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * OR HAVING
+	 *
+	 * Separates multiple calls with 'OR'.
+	 *
+	 * @param	string	$key
+	 * @param	string	$value
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	public function or_having($key, $value = NULL, $escape = NULL)
+	{
+		return $this->_wh('qb_having', $key, $value, 'OR ', $escape);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY
+	 *
+	 * @param	string	$orderby
+	 * @param	string	$direction	ASC, DESC or RANDOM
+	 * @param	bool	$escape
+	 * @return	CI_DB_query_builder
+	 */
+	public function order_by($orderby, $direction = '', $escape = NULL)
+	{
+		$direction = strtoupper(trim($direction));
+
+		if ($direction === 'RANDOM')
+		{
+			$direction = '';
+
+			// Do we have a seed value?
+			$orderby = ctype_digit((string) $orderby)
+				? sprintf($this->_random_keyword[1], $orderby)
+				: $this->_random_keyword[0];
+		}
+		elseif (empty($orderby))
+		{
+			return $this;
+		}
+		elseif ($direction !== '')
+		{
+			$direction = in_array($direction, array('ASC', 'DESC'), TRUE) ? ' '.$direction : '';
+		}
+
+		is_bool($escape) OR $escape = $this->_protect_identifiers;
+
+		if ($escape === FALSE)
+		{
+			$qb_orderby[] = array('field' => $orderby, 'direction' => $direction, 'escape' => FALSE);
+		}
+		else
+		{
+			$qb_orderby = array();
+			foreach (explode(',', $orderby) as $field)
+			{
+				$qb_orderby[] = ($direction === '' && preg_match('/\s+(ASC|DESC)$/i', rtrim($field), $match, PREG_OFFSET_CAPTURE))
+					? array('field' => ltrim(substr($field, 0, $match[0][1])), 'direction' => ' '.$match[1][0], 'escape' => TRUE)
+					: array('field' => trim($field), 'direction' => $direction, 'escape' => TRUE);
+			}
+		}
+
+		$this->qb_orderby = array_merge($this->qb_orderby, $qb_orderby);
+		if ($this->qb_caching === TRUE)
+		{
+			$this->qb_cache_orderby = array_merge($this->qb_cache_orderby, $qb_orderby);
+			$this->qb_cache_exists[] = 'orderby';
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT
+	 *
+	 * @param	int	$value	LIMIT value
+	 * @param	int	$offset	OFFSET value
+	 * @return	CI_DB_query_builder
+	 */
+	public function limit($value, $offset = 0)
+	{
+		is_null($value) OR $this->qb_limit = (int) $value;
+		empty($offset) OR $this->qb_offset = (int) $offset;
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Sets the OFFSET value
+	 *
+	 * @param	int	$offset	OFFSET value
+	 * @return	CI_DB_query_builder
+	 */
+	public function offset($offset)
+	{
+		empty($offset) OR $this->qb_offset = (int) $offset;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT string
+	 *
+	 * Generates a platform-specific LIMIT clause.
+	 *
+	 * @param	string	$sql	SQL Query
+	 * @return	string
+	 */
+	protected function _limit($sql)
+	{
+		return $sql.' LIMIT '.($this->qb_offset ? $this->qb_offset.', ' : '').(int) $this->qb_limit;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * The "set" function.
+	 *
+	 * Allows key/value pairs to be set for inserting or updating
+	 *
+	 * @param	mixed
+	 * @param	string
+	 * @param	bool
+	 * @return	CI_DB_query_builder
+	 */
+	public function set($key, $value = '', $escape = NULL)
+	{
+		$key = $this->_object_to_array($key);
+
+		if ( ! is_array($key))
+		{
+			$key = array($key => $value);
+		}
+
+		is_bool($escape) OR $escape = $this->_protect_identifiers;
+
+		foreach ($key as $k => $v)
+		{
+			$this->qb_set[$this->protect_identifiers($k, FALSE, $escape)] = ($escape)
+				? $this->escape($v) : $v;
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get SELECT query string
+	 *
+	 * Compiles a SELECT query string and returns the sql.
+	 *
+	 * @param	string	the table name to select from (optional)
+	 * @param	bool	TRUE: resets QB values; FALSE: leave QB values alone
+	 * @return	string
+	 */
+	public function get_compiled_select($table = '', $reset = TRUE)
+	{
+		if ($table !== '')
+		{
+			$this->_track_aliases($table);
+			$this->from($table);
+		}
+
+		$select = $this->_compile_select();
+
+		if ($reset === TRUE)
+		{
+			$this->_reset_select();
+		}
+
+		return $select;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get
+	 *
+	 * Compiles the select statement based on the other functions called
+	 * and runs the query
+	 *
+	 * @param	string	the table
+	 * @param	string	the limit clause
+	 * @param	string	the offset clause
+	 * @return	CI_DB_result
+	 */
+	public function get($table = '', $limit = NULL, $offset = NULL)
+	{
+		if ($table !== '')
+		{
+			$this->_track_aliases($table);
+			$this->from($table);
+		}
+
+		if ( ! empty($limit))
+		{
+			$this->limit($limit, $offset);
+		}
+
+		$result = $this->query($this->_compile_select());
+		$this->_reset_select();
+		return $result;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * "Count All Results" query
+	 *
+	 * Generates a platform-specific query string that counts all records
+	 * returned by an Query Builder query.
+	 *
+	 * @param	string
+	 * @param	bool	the reset clause
+	 * @return	int
+	 */
+	public function count_all_results($table = '', $reset = TRUE)
+	{
+		if ($table !== '')
+		{
+			$this->_track_aliases($table);
+			$this->from($table);
+		}
+
+		// ORDER BY usage is often problematic here (most notably
+		// on Microsoft SQL Server) and ultimately unnecessary
+		// for selecting COUNT(*) ...
+		$qb_orderby       = $this->qb_orderby;
+		$qb_cache_orderby = $this->qb_cache_orderby;
+		$this->qb_orderby = $this->qb_cache_orderby = array();
+
+		$result = ($this->qb_distinct === TRUE OR ! empty($this->qb_groupby) OR ! empty($this->qb_cache_groupby) OR ! empty($this->qb_having) OR $this->qb_limit OR $this->qb_offset)
+			? $this->query($this->_count_string.$this->protect_identifiers('numrows')."\nFROM (\n".$this->_compile_select()."\n) CI_count_all_results")
+			: $this->query($this->_compile_select($this->_count_string.$this->protect_identifiers('numrows')));
+
+		if ($reset === TRUE)
+		{
+			$this->_reset_select();
+		}
+		else
+		{
+			$this->qb_orderby       = $qb_orderby;
+			$this->qb_cache_orderby = $qb_cache_orderby;
+		}
+
+		if ($result->num_rows() === 0)
+		{
+			return 0;
+		}
+
+		$row = $result->row();
+		return (int) $row->numrows;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * get_where()
+	 *
+	 * Allows the where clause, limit and offset to be added directly
+	 *
+	 * @param	string	$table
+	 * @param	string	$where
+	 * @param	int	$limit
+	 * @param	int	$offset
+	 * @return	CI_DB_result
+	 */
+	public function get_where($table = '', $where = NULL, $limit = NULL, $offset = NULL)
+	{
+		if ($table !== '')
+		{
+			$this->from($table);
+		}
+
+		if ($where !== NULL)
+		{
+			$this->where($where);
+		}
+
+		if ( ! empty($limit))
+		{
+			$this->limit($limit, $offset);
+		}
+
+		$result = $this->query($this->_compile_select());
+		$this->_reset_select();
+		return $result;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert_Batch
+	 *
+	 * Compiles batch insert strings and runs the queries
+	 *
+	 * @param	string	$table	Table to insert into
+	 * @param	array	$set 	An associative array of insert values
+	 * @param	bool	$escape	Whether to escape values and identifiers
+	 * @return	int	Number of rows inserted or FALSE on failure
+	 */
+	public function insert_batch($table, $set = NULL, $escape = NULL, $batch_size = 100)
+	{
+		if ($set === NULL)
+		{
+			if (empty($this->qb_set))
+			{
+				return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE;
+			}
+		}
+		else
+		{
+			if (empty($set))
+			{
+				return ($this->db_debug) ? $this->display_error('insert_batch() called with no data') : FALSE;
+			}
+
+			$this->set_insert_batch($set, '', $escape);
+		}
+
+		if (strlen($table) === 0)
+		{
+			if ( ! isset($this->qb_from[0]))
+			{
+				return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE;
+			}
+
+			$table = $this->qb_from[0];
+		}
+
+		// Batch this baby
+		$affected_rows = 0;
+		for ($i = 0, $total = count($this->qb_set); $i < $total; $i += $batch_size)
+		{
+			if ($this->query($this->_insert_batch($this->protect_identifiers($table, TRUE, $escape, FALSE), $this->qb_keys, array_slice($this->qb_set, $i, $batch_size))))
+			{
+				$affected_rows += $this->affected_rows();
+			}
+		}
+
+		$this->_reset_write();
+		return $affected_rows;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert batch statement
+	 *
+	 * Generates a platform-specific insert string from the supplied data.
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$keys	INSERT keys
+	 * @param	array	$values	INSERT values
+	 * @return	string
+	 */
+	protected function _insert_batch($table, $keys, $values)
+	{
+		return 'INSERT INTO '.$table.' ('.implode(', ', $keys).') VALUES '.implode(', ', $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * The "set_insert_batch" function.  Allows key/value pairs to be set for batch inserts
+	 *
+	 * @param	mixed
+	 * @param	string
+	 * @param	bool
+	 * @return	CI_DB_query_builder
+	 */
+	public function set_insert_batch($key, $value = '', $escape = NULL)
+	{
+		$key = $this->_object_to_array_batch($key);
+
+		if ( ! is_array($key))
+		{
+			$key = array($key => $value);
+		}
+
+		is_bool($escape) OR $escape = $this->_protect_identifiers;
+
+		$keys = array_keys($this->_object_to_array(reset($key)));
+		sort($keys);
+
+		foreach ($key as $row)
+		{
+			$row = $this->_object_to_array($row);
+			if (count(array_diff($keys, array_keys($row))) > 0 OR count(array_diff(array_keys($row), $keys)) > 0)
+			{
+				// batch function above returns an error on an empty array
+				$this->qb_set[] = array();
+				return;
+			}
+
+			ksort($row); // puts $row in the same order as our keys
+
+			if ($escape !== FALSE)
+			{
+				$clean = array();
+				foreach ($row as $value)
+				{
+					$clean[] = $this->escape($value);
+				}
+
+				$row = $clean;
+			}
+
+			$this->qb_set[] = '('.implode(',', $row).')';
+		}
+
+		foreach ($keys as $k)
+		{
+			$this->qb_keys[] = $this->protect_identifiers($k, FALSE, $escape);
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get INSERT query string
+	 *
+	 * Compiles an insert query and returns the sql
+	 *
+	 * @param	string	the table to insert into
+	 * @param	bool	TRUE: reset QB values; FALSE: leave QB values alone
+	 * @return	string
+	 */
+	public function get_compiled_insert($table = '', $reset = TRUE)
+	{
+		if ($this->_validate_insert($table) === FALSE)
+		{
+			return FALSE;
+		}
+
+		$sql = $this->_insert(
+			$this->protect_identifiers(
+				$this->qb_from[0], TRUE, NULL, FALSE
+			),
+			array_keys($this->qb_set),
+			array_values($this->qb_set)
+		);
+
+		if ($reset === TRUE)
+		{
+			$this->_reset_write();
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert
+	 *
+	 * Compiles an insert string and runs the query
+	 *
+	 * @param	string	the table to insert data into
+	 * @param	array	an associative array of insert values
+	 * @param	bool	$escape	Whether to escape values and identifiers
+	 * @return	bool	TRUE on success, FALSE on failure
+	 */
+	public function insert($table = '', $set = NULL, $escape = NULL)
+	{
+		if ($set !== NULL)
+		{
+			$this->set($set, '', $escape);
+		}
+
+		if ($this->_validate_insert($table) === FALSE)
+		{
+			return FALSE;
+		}
+
+		$sql = $this->_insert(
+			$this->protect_identifiers(
+				$this->qb_from[0], TRUE, $escape, FALSE
+			),
+			array_keys($this->qb_set),
+			array_values($this->qb_set)
+		);
+
+		$this->_reset_write();
+		return $this->query($sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate Insert
+	 *
+	 * This method is used by both insert() and get_compiled_insert() to
+	 * validate that the there data is actually being set and that table
+	 * has been chosen to be inserted into.
+	 *
+	 * @param	string	the table to insert data into
+	 * @return	string
+	 */
+	protected function _validate_insert($table = '')
+	{
+		if (count($this->qb_set) === 0)
+		{
+			return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE;
+		}
+
+		if ($table !== '')
+		{
+			$this->qb_from[0] = $table;
+		}
+		elseif ( ! isset($this->qb_from[0]))
+		{
+			return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Replace
+	 *
+	 * Compiles an replace into string and runs the query
+	 *
+	 * @param	string	the table to replace data into
+	 * @param	array	an associative array of insert values
+	 * @return	bool	TRUE on success, FALSE on failure
+	 */
+	public function replace($table = '', $set = NULL)
+	{
+		if ($set !== NULL)
+		{
+			$this->set($set);
+		}
+
+		if (count($this->qb_set) === 0)
+		{
+			return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE;
+		}
+
+		if ($table === '')
+		{
+			if ( ! isset($this->qb_from[0]))
+			{
+				return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE;
+			}
+
+			$table = $this->qb_from[0];
+		}
+
+		$sql = $this->_replace($this->protect_identifiers($table, TRUE, NULL, FALSE), array_keys($this->qb_set), array_values($this->qb_set));
+
+		$this->_reset_write();
+		return $this->query($sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Replace statement
+	 *
+	 * Generates a platform-specific replace string from the supplied data
+	 *
+	 * @param	string	the table name
+	 * @param	array	the insert keys
+	 * @param	array	the insert values
+	 * @return	string
+	 */
+	protected function _replace($table, $keys, $values)
+	{
+		return 'REPLACE INTO '.$table.' ('.implode(', ', $keys).') VALUES ('.implode(', ', $values).')';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * FROM tables
+	 *
+	 * Groups tables in FROM clauses if needed, so there is no confusion
+	 * about operator precedence.
+	 *
+	 * Note: This is only used (and overridden) by MySQL and CUBRID.
+	 *
+	 * @return	string
+	 */
+	protected function _from_tables()
+	{
+		return implode(', ', $this->qb_from);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get UPDATE query string
+	 *
+	 * Compiles an update query and returns the sql
+	 *
+	 * @param	string	the table to update
+	 * @param	bool	TRUE: reset QB values; FALSE: leave QB values alone
+	 * @return	string
+	 */
+	public function get_compiled_update($table = '', $reset = TRUE)
+	{
+		// Combine any cached components with the current statements
+		$this->_merge_cache();
+
+		if ($this->_validate_update($table) === FALSE)
+		{
+			return FALSE;
+		}
+
+		$sql = $this->_update($this->qb_from[0], $this->qb_set);
+
+		if ($reset === TRUE)
+		{
+			$this->_reset_write();
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * UPDATE
+	 *
+	 * Compiles an update string and runs the query.
+	 *
+	 * @param	string	$table
+	 * @param	array	$set	An associative array of update values
+	 * @param	mixed	$where
+	 * @param	int	$limit
+	 * @return	bool	TRUE on success, FALSE on failure
+	 */
+	public function update($table = '', $set = NULL, $where = NULL, $limit = NULL)
+	{
+		// Combine any cached components with the current statements
+		$this->_merge_cache();
+
+		if ($set !== NULL)
+		{
+			$this->set($set);
+		}
+
+		if ($this->_validate_update($table) === FALSE)
+		{
+			return FALSE;
+		}
+
+		if ($where !== NULL)
+		{
+			$this->where($where);
+		}
+
+		if ( ! empty($limit))
+		{
+			$this->limit($limit);
+		}
+
+		$sql = $this->_update($this->qb_from[0], $this->qb_set);
+		$this->_reset_write();
+		return $this->query($sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate Update
+	 *
+	 * This method is used by both update() and get_compiled_update() to
+	 * validate that data is actually being set and that a table has been
+	 * chosen to be update.
+	 *
+	 * @param	string	the table to update data on
+	 * @return	bool
+	 */
+	protected function _validate_update($table)
+	{
+		if (count($this->qb_set) === 0)
+		{
+			return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE;
+		}
+
+		if ($table !== '')
+		{
+			$this->qb_from = array($this->protect_identifiers($table, TRUE, NULL, FALSE));
+		}
+		elseif ( ! isset($this->qb_from[0]))
+		{
+			return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update_Batch
+	 *
+	 * Compiles an update string and runs the query
+	 *
+	 * @param	string	the table to retrieve the results from
+	 * @param	array	an associative array of update values
+	 * @param	string	the where key
+	 * @return	int	number of rows affected or FALSE on failure
+	 */
+	public function update_batch($table, $set = NULL, $index = NULL, $batch_size = 100)
+	{
+		// Combine any cached components with the current statements
+		$this->_merge_cache();
+
+		if ($index === NULL)
+		{
+			return ($this->db_debug) ? $this->display_error('db_must_use_index') : FALSE;
+		}
+
+		if ($set === NULL)
+		{
+			if (empty($this->qb_set_ub))
+			{
+				return ($this->db_debug) ? $this->display_error('db_must_use_set') : FALSE;
+			}
+		}
+		else
+		{
+			if (empty($set))
+			{
+				return ($this->db_debug) ? $this->display_error('update_batch() called with no data') : FALSE;
+			}
+
+			$this->set_update_batch($set, $index);
+		}
+
+		if (strlen($table) === 0)
+		{
+			if ( ! isset($this->qb_from[0]))
+			{
+				return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE;
+			}
+
+			$table = $this->qb_from[0];
+		}
+
+		// Batch this baby
+		$affected_rows = 0;
+		for ($i = 0, $total = count($this->qb_set_ub); $i < $total; $i += $batch_size)
+		{
+			if ($this->query($this->_update_batch($this->protect_identifiers($table, TRUE, NULL, FALSE), array_slice($this->qb_set_ub, $i, $batch_size), $index)))
+			{
+				$affected_rows += $this->affected_rows();
+			}
+
+			$this->qb_where = array();
+		}
+
+		$this->_reset_write();
+		return $affected_rows;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update_Batch statement
+	 *
+	 * Generates a platform-specific batch update string from the supplied data
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$values	Update data
+	 * @param	string	$index	WHERE key
+	 * @return	string
+	 */
+	protected function _update_batch($table, $values, $index)
+	{
+		$ids = array();
+		foreach ($values as $key => $val)
+		{
+			$ids[] = $val[$index]['value'];
+
+			foreach (array_keys($val) as $field)
+			{
+				if ($field !== $index)
+				{
+					$final[$val[$field]['field']][] = 'WHEN '.$val[$index]['field'].' = '.$val[$index]['value'].' THEN '.$val[$field]['value'];
+				}
+			}
+		}
+
+		$cases = '';
+		foreach ($final as $k => $v)
+		{
+			$cases .= $k." = CASE \n"
+				.implode("\n", $v)."\n"
+				.'ELSE '.$k.' END, ';
+		}
+
+		$this->where($val[$index]['field'].' IN('.implode(',', $ids).')', NULL, FALSE);
+
+		return 'UPDATE '.$table.' SET '.substr($cases, 0, -2).$this->_compile_wh('qb_where');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * The "set_update_batch" function.  Allows key/value pairs to be set for batch updating
+	 *
+	 * @param	array
+	 * @param	string
+	 * @param	bool
+	 * @return	CI_DB_query_builder
+	 */
+	public function set_update_batch($key, $index = '', $escape = NULL)
+	{
+		$key = $this->_object_to_array_batch($key);
+
+		if ( ! is_array($key))
+		{
+			// @todo error
+		}
+
+		is_bool($escape) OR $escape = $this->_protect_identifiers;
+
+		foreach ($key as $k => $v)
+		{
+			$index_set = FALSE;
+			$clean = array();
+			foreach ($v as $k2 => $v2)
+			{
+				if ($k2 === $index)
+				{
+					$index_set = TRUE;
+				}
+
+				$clean[$k2] = array(
+					'field'  => $this->protect_identifiers($k2, FALSE, $escape),
+					'value'  => ($escape === FALSE ? $v2 : $this->escape($v2))
+				);
+			}
+
+			if ($index_set === FALSE)
+			{
+				return $this->display_error('db_batch_missing_index');
+			}
+
+			$this->qb_set_ub[] = $clean;
+		}
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Empty Table
+	 *
+	 * Compiles a delete string and runs "DELETE FROM table"
+	 *
+	 * @param	string	the table to empty
+	 * @return	bool	TRUE on success, FALSE on failure
+	 */
+	public function empty_table($table = '')
+	{
+		if ($table === '')
+		{
+			if ( ! isset($this->qb_from[0]))
+			{
+				return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE;
+			}
+
+			$table = $this->qb_from[0];
+		}
+		else
+		{
+			$table = $this->protect_identifiers($table, TRUE, NULL, FALSE);
+		}
+
+		$sql = $this->_delete($table);
+		$this->_reset_write();
+		return $this->query($sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Truncate
+	 *
+	 * Compiles a truncate string and runs the query
+	 * If the database does not support the truncate() command
+	 * This function maps to "DELETE FROM table"
+	 *
+	 * @param	string	the table to truncate
+	 * @return	bool	TRUE on success, FALSE on failure
+	 */
+	public function truncate($table = '')
+	{
+		if ($table === '')
+		{
+			if ( ! isset($this->qb_from[0]))
+			{
+				return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE;
+			}
+
+			$table = $this->qb_from[0];
+		}
+		else
+		{
+			$table = $this->protect_identifiers($table, TRUE, NULL, FALSE);
+		}
+
+		$sql = $this->_truncate($table);
+		$this->_reset_write();
+		return $this->query($sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Truncate statement
+	 *
+	 * Generates a platform-specific truncate string from the supplied data
+	 *
+	 * If the database does not support the truncate() command,
+	 * then this method maps to 'DELETE FROM table'
+	 *
+	 * @param	string	the table name
+	 * @return	string
+	 */
+	protected function _truncate($table)
+	{
+		return 'TRUNCATE '.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get DELETE query string
+	 *
+	 * Compiles a delete query string and returns the sql
+	 *
+	 * @param	string	the table to delete from
+	 * @param	bool	TRUE: reset QB values; FALSE: leave QB values alone
+	 * @return	string
+	 */
+	public function get_compiled_delete($table = '', $reset = TRUE)
+	{
+		$this->return_delete_sql = TRUE;
+		$sql = $this->delete($table, '', NULL, $reset);
+		$this->return_delete_sql = FALSE;
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete
+	 *
+	 * Compiles a delete string and runs the query
+	 *
+	 * @param	mixed	the table(s) to delete from. String or array
+	 * @param	mixed	the where clause
+	 * @param	mixed	the limit clause
+	 * @param	bool
+	 * @return	mixed
+	 */
+	public function delete($table = '', $where = '', $limit = NULL, $reset_data = TRUE)
+	{
+		// Combine any cached components with the current statements
+		$this->_merge_cache();
+
+		if ($table === '')
+		{
+			if ( ! isset($this->qb_from[0]))
+			{
+				return ($this->db_debug) ? $this->display_error('db_must_set_table') : FALSE;
+			}
+
+			$table = $this->qb_from[0];
+		}
+		elseif (is_array($table))
+		{
+			empty($where) && $reset_data = FALSE;
+
+			foreach ($table as $single_table)
+			{
+				$this->delete($single_table, $where, $limit, $reset_data);
+			}
+
+			return;
+		}
+		else
+		{
+			$table = $this->protect_identifiers($table, TRUE, NULL, FALSE);
+		}
+
+		if ($where !== '')
+		{
+			$this->where($where);
+		}
+
+		if ( ! empty($limit))
+		{
+			$this->limit($limit);
+		}
+
+		if (count($this->qb_where) === 0)
+		{
+			return ($this->db_debug) ? $this->display_error('db_del_must_use_where') : FALSE;
+		}
+
+		$sql = $this->_delete($table);
+		if ($reset_data)
+		{
+			$this->_reset_write();
+		}
+
+		return ($this->return_delete_sql === TRUE) ? $sql : $this->query($sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete statement
+	 *
+	 * Generates a platform-specific delete string from the supplied data
+	 *
+	 * @param	string	the table name
+	 * @return	string
+	 */
+	protected function _delete($table)
+	{
+		return 'DELETE FROM '.$table.$this->_compile_wh('qb_where')
+			.($this->qb_limit !== FALSE ? ' LIMIT '.$this->qb_limit : '');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * DB Prefix
+	 *
+	 * Prepends a database prefix if one exists in configuration
+	 *
+	 * @param	string	the table
+	 * @return	string
+	 */
+	public function dbprefix($table = '')
+	{
+		if ($table === '')
+		{
+			$this->display_error('db_table_name_required');
+		}
+
+		return $this->dbprefix.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set DB Prefix
+	 *
+	 * Set's the DB Prefix to something new without needing to reconnect
+	 *
+	 * @param	string	the prefix
+	 * @return	string
+	 */
+	public function set_dbprefix($prefix = '')
+	{
+		return $this->dbprefix = $prefix;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Track Aliases
+	 *
+	 * Used to track SQL statements written with aliased tables.
+	 *
+	 * @param	string	The table to inspect
+	 * @return	string
+	 */
+	protected function _track_aliases($table)
+	{
+		if (is_array($table))
+		{
+			foreach ($table as $t)
+			{
+				$this->_track_aliases($t);
+			}
+			return;
+		}
+
+		// Does the string contain a comma?  If so, we need to separate
+		// the string into discreet statements
+		if (strpos($table, ',') !== FALSE)
+		{
+			return $this->_track_aliases(explode(',', $table));
+		}
+
+		// if a table alias is used we can recognize it by a space
+		if (strpos($table, ' ') !== FALSE)
+		{
+			// if the alias is written with the AS keyword, remove it
+			$table = preg_replace('/\s+AS\s+/i', ' ', $table);
+
+			// Grab the alias
+			$table = trim(strrchr($table, ' '));
+
+			// Store the alias, if it doesn't already exist
+			if ( ! in_array($table, $this->qb_aliased_tables, TRUE))
+			{
+				$this->qb_aliased_tables[] = $table;
+				if ($this->qb_caching === TRUE && ! in_array($table, $this->qb_cache_aliased_tables, TRUE))
+				{
+					$this->qb_cache_aliased_tables[] = $table;
+					$this->qb_cache_exists[] = 'aliased_tables';
+				}
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile the SELECT statement
+	 *
+	 * Generates a query string based on which functions were used.
+	 * Should not be called directly.
+	 *
+	 * @param	bool	$select_override
+	 * @return	string
+	 */
+	protected function _compile_select($select_override = FALSE)
+	{
+		// Combine any cached components with the current statements
+		$this->_merge_cache();
+
+		// Write the "select" portion of the query
+		if ($select_override !== FALSE)
+		{
+			$sql = $select_override;
+		}
+		else
+		{
+			$sql = ( ! $this->qb_distinct) ? 'SELECT ' : 'SELECT DISTINCT ';
+
+			if (count($this->qb_select) === 0)
+			{
+				$sql .= '*';
+			}
+			else
+			{
+				// Cycle through the "select" portion of the query and prep each column name.
+				// The reason we protect identifiers here rather than in the select() function
+				// is because until the user calls the from() function we don't know if there are aliases
+				foreach ($this->qb_select as $key => $val)
+				{
+					$no_escape = isset($this->qb_no_escape[$key]) ? $this->qb_no_escape[$key] : NULL;
+					$this->qb_select[$key] = $this->protect_identifiers($val, FALSE, $no_escape);
+				}
+
+				$sql .= implode(', ', $this->qb_select);
+			}
+		}
+
+		// Write the "FROM" portion of the query
+		if (count($this->qb_from) > 0)
+		{
+			$sql .= "\nFROM ".$this->_from_tables();
+		}
+
+		// Write the "JOIN" portion of the query
+		if (count($this->qb_join) > 0)
+		{
+			$sql .= "\n".implode("\n", $this->qb_join);
+		}
+
+		$sql .= $this->_compile_wh('qb_where')
+			.$this->_compile_group_by()
+			.$this->_compile_wh('qb_having')
+			.$this->_compile_order_by(); // ORDER BY
+
+		// LIMIT
+		if ($this->qb_limit !== FALSE OR $this->qb_offset)
+		{
+			return $this->_limit($sql."\n");
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile WHERE, HAVING statements
+	 *
+	 * Escapes identifiers in WHERE and HAVING statements at execution time.
+	 *
+	 * Required so that aliases are tracked properly, regardless of whether
+	 * where(), or_where(), having(), or_having are called prior to from(),
+	 * join() and dbprefix is added only if needed.
+	 *
+	 * @param	string	$qb_key	'qb_where' or 'qb_having'
+	 * @return	string	SQL statement
+	 */
+	protected function _compile_wh($qb_key)
+	{
+		if (count($this->$qb_key) > 0)
+		{
+			for ($i = 0, $c = count($this->$qb_key); $i < $c; $i++)
+			{
+				// Is this condition already compiled?
+				if (is_string($this->{$qb_key}[$i]))
+				{
+					continue;
+				}
+				elseif ($this->{$qb_key}[$i]['escape'] === FALSE)
+				{
+					$this->{$qb_key}[$i] = $this->{$qb_key}[$i]['condition'].(isset($this->{$qb_key}[$i]['value']) ? ' '.$this->{$qb_key}[$i]['value'] : '');
+					continue;
+				}
+
+				// Split multiple conditions
+				$conditions = preg_split(
+					'/((?:^|\s+)AND\s+|(?:^|\s+)OR\s+)/i',
+					$this->{$qb_key}[$i]['condition'],
+					-1,
+					PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
+				);
+
+				for ($ci = 0, $cc = count($conditions); $ci < $cc; $ci++)
+				{
+					if (($op = $this->_get_operator($conditions[$ci])) === FALSE
+						OR ! preg_match('/^(\(?)(.*)('.preg_quote($op, '/').')\s*(.*(?<!\)))?(\)?)$/i', $conditions[$ci], $matches))
+					{
+						continue;
+					}
+
+					// $matches = array(
+					//	0 => '(test <= foo)',	/* the whole thing */
+					//	1 => '(',		/* optional */
+					//	2 => 'test',		/* the field name */
+					//	3 => ' <= ',		/* $op */
+					//	4 => 'foo',		/* optional, if $op is e.g. 'IS NULL' */
+					//	5 => ')'		/* optional */
+					// );
+
+					if ( ! empty($matches[4]))
+					{
+						$this->_is_literal($matches[4]) OR $matches[4] = $this->protect_identifiers(trim($matches[4]));
+						$matches[4] = ' '.$matches[4];
+					}
+
+					$conditions[$ci] = $matches[1].$this->protect_identifiers(trim($matches[2]))
+						.' '.trim($matches[3]).$matches[4].$matches[5];
+				}
+
+				$this->{$qb_key}[$i] = implode('', $conditions).(isset($this->{$qb_key}[$i]['value']) ? ' '.$this->{$qb_key}[$i]['value'] : '');
+			}
+
+			return ($qb_key === 'qb_having' ? "\nHAVING " : "\nWHERE ")
+				.implode("\n", $this->$qb_key);
+		}
+
+		return '';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile GROUP BY
+	 *
+	 * Escapes identifiers in GROUP BY statements at execution time.
+	 *
+	 * Required so that aliases are tracked properly, regardless of whether
+	 * group_by() is called prior to from(), join() and dbprefix is added
+	 * only if needed.
+	 *
+	 * @return	string	SQL statement
+	 */
+	protected function _compile_group_by()
+	{
+		if (count($this->qb_groupby) > 0)
+		{
+			for ($i = 0, $c = count($this->qb_groupby); $i < $c; $i++)
+			{
+				// Is it already compiled?
+				if (is_string($this->qb_groupby[$i]))
+				{
+					continue;
+				}
+
+				$this->qb_groupby[$i] = ($this->qb_groupby[$i]['escape'] === FALSE OR $this->_is_literal($this->qb_groupby[$i]['field']))
+					? $this->qb_groupby[$i]['field']
+					: $this->protect_identifiers($this->qb_groupby[$i]['field']);
+			}
+
+			return "\nGROUP BY ".implode(', ', $this->qb_groupby);
+		}
+
+		return '';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile ORDER BY
+	 *
+	 * Escapes identifiers in ORDER BY statements at execution time.
+	 *
+	 * Required so that aliases are tracked properly, regardless of whether
+	 * order_by() is called prior to from(), join() and dbprefix is added
+	 * only if needed.
+	 *
+	 * @return	string	SQL statement
+	 */
+	protected function _compile_order_by()
+	{
+		if (empty($this->qb_orderby))
+		{
+			return '';
+		}
+
+		for ($i = 0, $c = count($this->qb_orderby); $i < $c; $i++)
+		{
+			if (is_string($this->qb_orderby[$i]))
+			{
+				continue;
+			}
+
+			if ($this->qb_orderby[$i]['escape'] !== FALSE && ! $this->_is_literal($this->qb_orderby[$i]['field']))
+			{
+				$this->qb_orderby[$i]['field'] = $this->protect_identifiers($this->qb_orderby[$i]['field']);
+			}
+
+			$this->qb_orderby[$i] = $this->qb_orderby[$i]['field'].$this->qb_orderby[$i]['direction'];
+		}
+
+		return "\nORDER BY ".implode(', ', $this->qb_orderby);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Object to Array
+	 *
+	 * Takes an object as input and converts the class variables to array key/vals
+	 *
+	 * @param	object
+	 * @return	array
+	 */
+	protected function _object_to_array($object)
+	{
+		if ( ! is_object($object))
+		{
+			return $object;
+		}
+
+		$array = array();
+		foreach (get_object_vars($object) as $key => $val)
+		{
+			// There are some built in keys we need to ignore for this conversion
+			if ( ! is_object($val) && ! is_array($val) && $key !== '_parent_name')
+			{
+				$array[$key] = $val;
+			}
+		}
+
+		return $array;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Object to Array
+	 *
+	 * Takes an object as input and converts the class variables to array key/vals
+	 *
+	 * @param	object
+	 * @return	array
+	 */
+	protected function _object_to_array_batch($object)
+	{
+		if ( ! is_object($object))
+		{
+			return $object;
+		}
+
+		$array = array();
+		$out = get_object_vars($object);
+		$fields = array_keys($out);
+
+		foreach ($fields as $val)
+		{
+			// There are some built in keys we need to ignore for this conversion
+			if ($val !== '_parent_name')
+			{
+				$i = 0;
+				foreach ($out[$val] as $data)
+				{
+					$array[$i++][$val] = $data;
+				}
+			}
+		}
+
+		return $array;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Start Cache
+	 *
+	 * Starts QB caching
+	 *
+	 * @return	CI_DB_query_builder
+	 */
+	public function start_cache()
+	{
+		$this->qb_caching = TRUE;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Stop Cache
+	 *
+	 * Stops QB caching
+	 *
+	 * @return	CI_DB_query_builder
+	 */
+	public function stop_cache()
+	{
+		$this->qb_caching = FALSE;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Flush Cache
+	 *
+	 * Empties the QB cache
+	 *
+	 * @return	CI_DB_query_builder
+	 */
+	public function flush_cache()
+	{
+		$this->_reset_run(array(
+			'qb_cache_select'		=> array(),
+			'qb_cache_from'			=> array(),
+			'qb_cache_join'			=> array(),
+			'qb_cache_where'		=> array(),
+			'qb_cache_groupby'		=> array(),
+			'qb_cache_having'		=> array(),
+			'qb_cache_orderby'		=> array(),
+			'qb_cache_set'			=> array(),
+			'qb_cache_exists'		=> array(),
+			'qb_cache_no_escape'	=> array(),
+			'qb_cache_aliased_tables'	=> array()
+		));
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Merge Cache
+	 *
+	 * When called, this function merges any cached QB arrays with
+	 * locally called ones.
+	 *
+	 * @return	void
+	 */
+	protected function _merge_cache()
+	{
+		if (count($this->qb_cache_exists) === 0)
+		{
+			return;
+		}
+		elseif (in_array('select', $this->qb_cache_exists, TRUE))
+		{
+			$qb_no_escape = $this->qb_cache_no_escape;
+		}
+
+		foreach (array_unique($this->qb_cache_exists) as $val) // select, from, etc.
+		{
+			$qb_variable	= 'qb_'.$val;
+			$qb_cache_var	= 'qb_cache_'.$val;
+			$qb_new 	= $this->$qb_cache_var;
+
+			for ($i = 0, $c = count($this->$qb_variable); $i < $c; $i++)
+			{
+				if ( ! in_array($this->{$qb_variable}[$i], $qb_new, TRUE))
+				{
+					$qb_new[] = $this->{$qb_variable}[$i];
+					if ($val === 'select')
+					{
+						$qb_no_escape[] = $this->qb_no_escape[$i];
+					}
+				}
+			}
+
+			$this->$qb_variable = $qb_new;
+			if ($val === 'select')
+			{
+				$this->qb_no_escape = $qb_no_escape;
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Is literal
+	 *
+	 * Determines if a string represents a literal value or a field name
+	 *
+	 * @param	string	$str
+	 * @return	bool
+	 */
+	protected function _is_literal($str)
+	{
+		$str = trim($str);
+
+		if (empty($str) OR ctype_digit($str) OR (string) (float) $str === $str OR in_array(strtoupper($str), array('TRUE', 'FALSE'), TRUE))
+		{
+			return TRUE;
+		}
+
+		static $_str;
+
+		if (empty($_str))
+		{
+			$_str = ($this->_escape_char !== '"')
+				? array('"', "'") : array("'");
+		}
+
+		return in_array($str[0], $_str, TRUE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Reset Query Builder values.
+	 *
+	 * Publicly-visible method to reset the QB values.
+	 *
+	 * @return	CI_DB_query_builder
+	 */
+	public function reset_query()
+	{
+		$this->_reset_select();
+		$this->_reset_write();
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Resets the query builder values.  Called by the get() function
+	 *
+	 * @param	array	An array of fields to reset
+	 * @return	void
+	 */
+	protected function _reset_run($qb_reset_items)
+	{
+		foreach ($qb_reset_items as $item => $default_value)
+		{
+			$this->$item = $default_value;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Resets the query builder values.  Called by the get() function
+	 *
+	 * @return	void
+	 */
+	protected function _reset_select()
+	{
+		$this->_reset_run(array(
+			'qb_select'		=> array(),
+			'qb_from'		=> array(),
+			'qb_join'		=> array(),
+			'qb_where'		=> array(),
+			'qb_groupby'		=> array(),
+			'qb_having'		=> array(),
+			'qb_orderby'		=> array(),
+			'qb_aliased_tables'	=> array(),
+			'qb_no_escape'		=> array(),
+			'qb_distinct'		=> FALSE,
+			'qb_limit'		=> FALSE,
+			'qb_offset'		=> FALSE
+		));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Resets the query builder "write" values.
+	 *
+	 * Called by the insert() update() insert_batch() update_batch() and delete() functions
+	 *
+	 * @return	void
+	 */
+	protected function _reset_write()
+	{
+		$this->_reset_run(array(
+			'qb_set'	=> array(),
+			'qb_set_ub'	=> array(),
+			'qb_from'	=> array(),
+			'qb_join'	=> array(),
+			'qb_where'	=> array(),
+			'qb_orderby'	=> array(),
+			'qb_keys'	=> array(),
+			'qb_limit'	=> FALSE
+		));
+	}
+
+}
diff --git a/system/database/DB_result.php b/system/database/DB_result.php
new file mode 100644
index 0000000..94da294
--- /dev/null
+++ b/system/database/DB_result.php
@@ -0,0 +1,666 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Database Result Class
+ *
+ * This is the platform-independent result class.
+ * This class will not be called directly. Rather, the adapter
+ * class for the specific database will extend and instantiate it.
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_result {
+
+	/**
+	 * Connection ID
+	 *
+	 * @var	resource|object
+	 */
+	public $conn_id;
+
+	/**
+	 * Result ID
+	 *
+	 * @var	resource|object
+	 */
+	public $result_id;
+
+	/**
+	 * Result Array
+	 *
+	 * @var	array[]
+	 */
+	public $result_array			= array();
+
+	/**
+	 * Result Object
+	 *
+	 * @var	object[]
+	 */
+	public $result_object			= array();
+
+	/**
+	 * Custom Result Object
+	 *
+	 * @var	object[]
+	 */
+	public $custom_result_object		= array();
+
+	/**
+	 * Current Row index
+	 *
+	 * @var	int
+	 */
+	public $current_row			= 0;
+
+	/**
+	 * Number of rows
+	 *
+	 * @var	int
+	 */
+	public $num_rows;
+
+	/**
+	 * Row data
+	 *
+	 * @var	array
+	 */
+	public $row_data;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * @param	object	$driver_object
+	 * @return	void
+	 */
+	public function __construct(&$driver_object)
+	{
+		$this->conn_id = $driver_object->conn_id;
+		$this->result_id = $driver_object->result_id;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Number of rows in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_rows()
+	{
+		if (is_int($this->num_rows))
+		{
+			return $this->num_rows;
+		}
+		elseif (count($this->result_array) > 0)
+		{
+			return $this->num_rows = count($this->result_array);
+		}
+		elseif (count($this->result_object) > 0)
+		{
+			return $this->num_rows = count($this->result_object);
+		}
+
+		return $this->num_rows = count($this->result_array());
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Query result. Acts as a wrapper function for the following functions.
+	 *
+	 * @param	string	$type	'object', 'array' or a custom class name
+	 * @return	array
+	 */
+	public function result($type = 'object')
+	{
+		if ($type === 'array')
+		{
+			return $this->result_array();
+		}
+		elseif ($type === 'object')
+		{
+			return $this->result_object();
+		}
+
+		return $this->custom_result_object($type);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Custom query result.
+	 *
+	 * @param	string	$class_name
+	 * @return	array
+	 */
+	public function custom_result_object($class_name)
+	{
+		if (isset($this->custom_result_object[$class_name]))
+		{
+			return $this->custom_result_object[$class_name];
+		}
+		elseif ( ! $this->result_id OR $this->num_rows === 0)
+		{
+			return array();
+		}
+
+		// Don't fetch the result set again if we already have it
+		$_data = NULL;
+		if (($c = count($this->result_array)) > 0)
+		{
+			$_data = 'result_array';
+		}
+		elseif (($c = count($this->result_object)) > 0)
+		{
+			$_data = 'result_object';
+		}
+
+		if ($_data !== NULL)
+		{
+			for ($i = 0; $i < $c; $i++)
+			{
+				$this->custom_result_object[$class_name][$i] = new $class_name();
+
+				foreach ($this->{$_data}[$i] as $key => $value)
+				{
+					$this->custom_result_object[$class_name][$i]->$key = $value;
+				}
+			}
+
+			return $this->custom_result_object[$class_name];
+		}
+
+		is_null($this->row_data) OR $this->data_seek(0);
+		$this->custom_result_object[$class_name] = array();
+
+		while ($row = $this->_fetch_object($class_name))
+		{
+			$this->custom_result_object[$class_name][] = $row;
+		}
+
+		return $this->custom_result_object[$class_name];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Query result. "object" version.
+	 *
+	 * @return	array
+	 */
+	public function result_object()
+	{
+		if (count($this->result_object) > 0)
+		{
+			return $this->result_object;
+		}
+
+		// In the event that query caching is on, the result_id variable
+		// will not be a valid resource so we'll simply return an empty
+		// array.
+		if ( ! $this->result_id OR $this->num_rows === 0)
+		{
+			return array();
+		}
+
+		if (($c = count($this->result_array)) > 0)
+		{
+			for ($i = 0; $i < $c; $i++)
+			{
+				$this->result_object[$i] = (object) $this->result_array[$i];
+			}
+
+			return $this->result_object;
+		}
+
+		is_null($this->row_data) OR $this->data_seek(0);
+		while ($row = $this->_fetch_object())
+		{
+			$this->result_object[] = $row;
+		}
+
+		return $this->result_object;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Query result. "array" version.
+	 *
+	 * @return	array
+	 */
+	public function result_array()
+	{
+		if (count($this->result_array) > 0)
+		{
+			return $this->result_array;
+		}
+
+		// In the event that query caching is on, the result_id variable
+		// will not be a valid resource so we'll simply return an empty
+		// array.
+		if ( ! $this->result_id OR $this->num_rows === 0)
+		{
+			return array();
+		}
+
+		if (($c = count($this->result_object)) > 0)
+		{
+			for ($i = 0; $i < $c; $i++)
+			{
+				$this->result_array[$i] = (array) $this->result_object[$i];
+			}
+
+			return $this->result_array;
+		}
+
+		is_null($this->row_data) OR $this->data_seek(0);
+		while ($row = $this->_fetch_assoc())
+		{
+			$this->result_array[] = $row;
+		}
+
+		return $this->result_array;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Row
+	 *
+	 * A wrapper method.
+	 *
+	 * @param	mixed	$n
+	 * @param	string	$type	'object' or 'array'
+	 * @return	mixed
+	 */
+	public function row($n = 0, $type = 'object')
+	{
+		if ( ! is_numeric($n))
+		{
+			// We cache the row data for subsequent uses
+			is_array($this->row_data) OR $this->row_data = $this->row_array(0);
+
+			// array_key_exists() instead of isset() to allow for NULL values
+			if (empty($this->row_data) OR ! array_key_exists($n, $this->row_data))
+			{
+				return NULL;
+			}
+
+			return $this->row_data[$n];
+		}
+
+		if ($type === 'object') return $this->row_object($n);
+		elseif ($type === 'array') return $this->row_array($n);
+
+		return $this->custom_row_object($n, $type);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Assigns an item into a particular column slot
+	 *
+	 * @param	mixed	$key
+	 * @param	mixed	$value
+	 * @return	void
+	 */
+	public function set_row($key, $value = NULL)
+	{
+		// We cache the row data for subsequent uses
+		if ( ! is_array($this->row_data))
+		{
+			$this->row_data = $this->row_array(0);
+		}
+
+		if (is_array($key))
+		{
+			foreach ($key as $k => $v)
+			{
+				$this->row_data[$k] = $v;
+			}
+			return;
+		}
+
+		if ($key !== '' && $value !== NULL)
+		{
+			$this->row_data[$key] = $value;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns a single result row - custom object version
+	 *
+	 * @param	int	$n
+	 * @param	string	$type
+	 * @return	object
+	 */
+	public function custom_row_object($n, $type)
+	{
+		isset($this->custom_result_object[$type]) OR $this->custom_result_object[$type] = $this->custom_result_object($type);
+
+		if (count($this->custom_result_object[$type]) === 0)
+		{
+			return NULL;
+		}
+
+		if ($n !== $this->current_row && isset($this->custom_result_object[$type][$n]))
+		{
+			$this->current_row = $n;
+		}
+
+		return $this->custom_result_object[$type][$this->current_row];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns a single result row - object version
+	 *
+	 * @param	int	$n
+	 * @return	object
+	 */
+	public function row_object($n = 0)
+	{
+		$result = $this->result_object();
+		if (count($result) === 0)
+		{
+			return NULL;
+		}
+
+		if ($n !== $this->current_row && isset($result[$n]))
+		{
+			$this->current_row = $n;
+		}
+
+		return $result[$this->current_row];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns a single result row - array version
+	 *
+	 * @param	int	$n
+	 * @return	array
+	 */
+	public function row_array($n = 0)
+	{
+		$result = $this->result_array();
+		if (count($result) === 0)
+		{
+			return NULL;
+		}
+
+		if ($n !== $this->current_row && isset($result[$n]))
+		{
+			$this->current_row = $n;
+		}
+
+		return $result[$this->current_row];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns the "first" row
+	 *
+	 * @param	string	$type
+	 * @return	mixed
+	 */
+	public function first_row($type = 'object')
+	{
+		$result = $this->result($type);
+		return (count($result) === 0) ? NULL : $result[0];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns the "last" row
+	 *
+	 * @param	string	$type
+	 * @return	mixed
+	 */
+	public function last_row($type = 'object')
+	{
+		$result = $this->result($type);
+		return (count($result) === 0) ? NULL : $result[count($result) - 1];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns the "next" row
+	 *
+	 * @param	string	$type
+	 * @return	mixed
+	 */
+	public function next_row($type = 'object')
+	{
+		$result = $this->result($type);
+		if (count($result) === 0)
+		{
+			return NULL;
+		}
+
+		return isset($result[$this->current_row + 1])
+			? $result[++$this->current_row]
+			: NULL;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns the "previous" row
+	 *
+	 * @param	string	$type
+	 * @return	mixed
+	 */
+	public function previous_row($type = 'object')
+	{
+		$result = $this->result($type);
+		if (count($result) === 0)
+		{
+			return NULL;
+		}
+
+		if (isset($result[$this->current_row - 1]))
+		{
+			--$this->current_row;
+		}
+		return $result[$this->current_row];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an unbuffered row and move pointer to next row
+	 *
+	 * @param	string	$type	'array', 'object' or a custom class name
+	 * @return	mixed
+	 */
+	public function unbuffered_row($type = 'object')
+	{
+		if ($type === 'array')
+		{
+			return $this->_fetch_assoc();
+		}
+		elseif ($type === 'object')
+		{
+			return $this->_fetch_object();
+		}
+
+		return $this->_fetch_object($type);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * The following methods are normally overloaded by the identically named
+	 * methods in the platform-specific driver -- except when query caching
+	 * is used. When caching is enabled we do not load the other driver.
+	 * These functions are primarily here to prevent undefined function errors
+	 * when a cached result object is in use. They are not otherwise fully
+	 * operational due to the unavailability of the database resource IDs with
+	 * cached results.
+	 */
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Number of fields in the result set
+	 *
+	 * Overridden by driver result classes.
+	 *
+	 * @return	int
+	 */
+	public function num_fields()
+	{
+		return 0;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * Generates an array of column names.
+	 *
+	 * Overridden by driver result classes.
+	 *
+	 * @return	array
+	 */
+	public function list_fields()
+	{
+		return array();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data
+	 *
+	 * Generates an array of objects containing field meta-data.
+	 *
+	 * Overridden by driver result classes.
+	 *
+	 * @return	array
+	 */
+	public function field_data()
+	{
+		return array();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Free the result
+	 *
+	 * Overridden by driver result classes.
+	 *
+	 * @return	void
+	 */
+	public function free_result()
+	{
+		$this->result_id = FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Data Seek
+	 *
+	 * Moves the internal pointer to the desired offset. We call
+	 * this internally before fetching results to make sure the
+	 * result set starts at zero.
+	 *
+	 * Overridden by driver result classes.
+	 *
+	 * @param	int	$n
+	 * @return	bool
+	 */
+	public function data_seek($n = 0)
+	{
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - associative array
+	 *
+	 * Returns the result set as an array.
+	 *
+	 * Overridden by driver result classes.
+	 *
+	 * @return	array
+	 */
+	protected function _fetch_assoc()
+	{
+		return array();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - object
+	 *
+	 * Returns the result set as an object.
+	 *
+	 * Overridden by driver result classes.
+	 *
+	 * @param	string	$class_name
+	 * @return	object
+	 */
+	protected function _fetch_object($class_name = 'stdClass')
+	{
+		return new $class_name();
+	}
+
+}
diff --git a/system/database/DB_utility.php b/system/database/DB_utility.php
new file mode 100644
index 0000000..11aa67b
--- /dev/null
+++ b/system/database/DB_utility.php
@@ -0,0 +1,425 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Database Utility Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+abstract class CI_DB_utility {
+
+	/**
+	 * Database object
+	 *
+	 * @var	object
+	 */
+	protected $db;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * List databases statement
+	 *
+	 * @var	string
+	 */
+	protected $_list_databases		= FALSE;
+
+	/**
+	 * OPTIMIZE TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_optimize_table	= FALSE;
+
+	/**
+	 * REPAIR TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_repair_table	= FALSE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	object	&$db	Database object
+	 * @return	void
+	 */
+	public function __construct(&$db)
+	{
+		$this->db =& $db;
+		log_message('info', 'Database Utility Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * List databases
+	 *
+	 * @return	array
+	 */
+	public function list_databases()
+	{
+		// Is there a cached result?
+		if (isset($this->db->data_cache['db_names']))
+		{
+			return $this->db->data_cache['db_names'];
+		}
+		elseif ($this->_list_databases === FALSE)
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE;
+		}
+
+		$this->db->data_cache['db_names'] = array();
+
+		$query = $this->db->query($this->_list_databases);
+		if ($query === FALSE)
+		{
+			return $this->db->data_cache['db_names'];
+		}
+
+		for ($i = 0, $query = $query->result_array(), $c = count($query); $i < $c; $i++)
+		{
+			$this->db->data_cache['db_names'][] = current($query[$i]);
+		}
+
+		return $this->db->data_cache['db_names'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Determine if a particular database exists
+	 *
+	 * @param	string	$database_name
+	 * @return	bool
+	 */
+	public function database_exists($database_name)
+	{
+		return in_array($database_name, $this->list_databases());
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Optimize Table
+	 *
+	 * @param	string	$table_name
+	 * @return	mixed
+	 */
+	public function optimize_table($table_name)
+	{
+		if ($this->_optimize_table === FALSE)
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE;
+		}
+
+		$query = $this->db->query(sprintf($this->_optimize_table, $this->db->escape_identifiers($table_name)));
+		if ($query !== FALSE)
+		{
+			$query = $query->result_array();
+			return current($query);
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Optimize Database
+	 *
+	 * @return	mixed
+	 */
+	public function optimize_database()
+	{
+		if ($this->_optimize_table === FALSE)
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE;
+		}
+
+		$result = array();
+		foreach ($this->db->list_tables() as $table_name)
+		{
+			$res = $this->db->query(sprintf($this->_optimize_table, $this->db->escape_identifiers($table_name)));
+			if (is_bool($res))
+			{
+				return $res;
+			}
+
+			// Build the result array...
+			$res = $res->result_array();
+			$res = current($res);
+			$key = str_replace($this->db->database.'.', '', current($res));
+			$keys = array_keys($res);
+			unset($res[$keys[0]]);
+
+			$result[$key] = $res;
+		}
+
+		return $result;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Repair Table
+	 *
+	 * @param	string	$table_name
+	 * @return	mixed
+	 */
+	public function repair_table($table_name)
+	{
+		if ($this->_repair_table === FALSE)
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unsupported_feature') : FALSE;
+		}
+
+		$query = $this->db->query(sprintf($this->_repair_table, $this->db->escape_identifiers($table_name)));
+		if (is_bool($query))
+		{
+			return $query;
+		}
+
+		$query = $query->result_array();
+		return current($query);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Generate CSV from a query result object
+	 *
+	 * @param	object	$query		Query result object
+	 * @param	string	$delim		Delimiter (default: ,)
+	 * @param	string	$newline	Newline character (default: \n)
+	 * @param	string	$enclosure	Enclosure (default: ")
+	 * @return	string
+	 */
+	public function csv_from_result($query, $delim = ',', $newline = "\n", $enclosure = '"')
+	{
+		if ( ! is_object($query) OR ! method_exists($query, 'list_fields'))
+		{
+			show_error('You must submit a valid result object');
+		}
+
+		$out = '';
+		// First generate the headings from the table column names
+		foreach ($query->list_fields() as $name)
+		{
+			$out .= $enclosure.str_replace($enclosure, $enclosure.$enclosure, $name).$enclosure.$delim;
+		}
+
+		$out = substr($out, 0, -strlen($delim)).$newline;
+
+		// Next blast through the result array and build out the rows
+		while ($row = $query->unbuffered_row('array'))
+		{
+			$line = array();
+			foreach ($row as $item)
+			{
+				$line[] = $enclosure.str_replace($enclosure, $enclosure.$enclosure, (string) $item).$enclosure;
+			}
+			$out .= implode($delim, $line).$newline;
+		}
+
+		return $out;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Generate XML data from a query result object
+	 *
+	 * @param	object	$query	Query result object
+	 * @param	array	$params	Any preferences
+	 * @return	string
+	 */
+	public function xml_from_result($query, $params = array())
+	{
+		if ( ! is_object($query) OR ! method_exists($query, 'list_fields'))
+		{
+			show_error('You must submit a valid result object');
+		}
+
+		// Set our default values
+		foreach (array('root' => 'root', 'element' => 'element', 'newline' => "\n", 'tab' => "\t") as $key => $val)
+		{
+			if ( ! isset($params[$key]))
+			{
+				$params[$key] = $val;
+			}
+		}
+
+		// Create variables for convenience
+		extract($params);
+
+		// Load the xml helper
+		get_instance()->load->helper('xml');
+
+		// Generate the result
+		$xml = '<'.$root.'>'.$newline;
+		while ($row = $query->unbuffered_row())
+		{
+			$xml .= $tab.'<'.$element.'>'.$newline;
+			foreach ($row as $key => $val)
+			{
+				$xml .= $tab.$tab.'<'.$key.'>'.xml_convert($val).'</'.$key.'>'.$newline;
+			}
+			$xml .= $tab.'</'.$element.'>'.$newline;
+		}
+
+		return $xml.'</'.$root.'>'.$newline;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database Backup
+	 *
+	 * @param	array	$params
+	 * @return	string
+	 */
+	public function backup($params = array())
+	{
+		// If the parameters have not been submitted as an
+		// array then we know that it is simply the table
+		// name, which is a valid short cut.
+		if (is_string($params))
+		{
+			$params = array('tables' => $params);
+		}
+
+		// Set up our default preferences
+		$prefs = array(
+			'tables'		=> array(),
+			'ignore'		=> array(),
+			'filename'		=> '',
+			'format'		=> 'gzip', // gzip, zip, txt
+			'add_drop'		=> TRUE,
+			'add_insert'		=> TRUE,
+			'newline'		=> "\n",
+			'foreign_key_checks'	=> TRUE
+		);
+
+		// Did the user submit any preferences? If so set them....
+		if (count($params) > 0)
+		{
+			foreach ($prefs as $key => $val)
+			{
+				if (isset($params[$key]))
+				{
+					$prefs[$key] = $params[$key];
+				}
+			}
+		}
+
+		// Are we backing up a complete database or individual tables?
+		// If no table names were submitted we'll fetch the entire table list
+		if (count($prefs['tables']) === 0)
+		{
+			$prefs['tables'] = $this->db->list_tables();
+		}
+
+		// Validate the format
+		if ( ! in_array($prefs['format'], array('gzip', 'zip', 'txt'), TRUE))
+		{
+			$prefs['format'] = 'txt';
+		}
+
+		// Is the encoder supported? If not, we'll either issue an
+		// error or use plain text depending on the debug settings
+		if (($prefs['format'] === 'gzip' && ! function_exists('gzencode'))
+			OR ($prefs['format'] === 'zip' && ! function_exists('gzcompress')))
+		{
+			if ($this->db->db_debug)
+			{
+				return $this->db->display_error('db_unsupported_compression');
+			}
+
+			$prefs['format'] = 'txt';
+		}
+
+		// Was a Zip file requested?
+		if ($prefs['format'] === 'zip')
+		{
+			// Set the filename if not provided (only needed with Zip files)
+			if ($prefs['filename'] === '')
+			{
+				$prefs['filename'] = (count($prefs['tables']) === 1 ? $prefs['tables'] : $this->db->database)
+							.date('Y-m-d_H-i', time()).'.sql';
+			}
+			else
+			{
+				// If they included the .zip file extension we'll remove it
+				if (preg_match('|.+?\.zip$|', $prefs['filename']))
+				{
+					$prefs['filename'] = str_replace('.zip', '', $prefs['filename']);
+				}
+
+				// Tack on the ".sql" file extension if needed
+				if ( ! preg_match('|.+?\.sql$|', $prefs['filename']))
+				{
+					$prefs['filename'] .= '.sql';
+				}
+			}
+
+			// Load the Zip class and output it
+			$CI =& get_instance();
+			$CI->load->library('zip');
+			$CI->zip->add_data($prefs['filename'], $this->_backup($prefs));
+			return $CI->zip->get_zip();
+		}
+		elseif ($prefs['format'] === 'txt') // Was a text file requested?
+		{
+			return $this->_backup($prefs);
+		}
+		elseif ($prefs['format'] === 'gzip') // Was a Gzip file requested?
+		{
+			return gzencode($this->_backup($prefs));
+		}
+
+		return;
+	}
+
+}
diff --git a/system/database/drivers/cubrid/cubrid_driver.php b/system/database/drivers/cubrid/cubrid_driver.php
new file mode 100644
index 0000000..bd01be6
--- /dev/null
+++ b/system/database/drivers/cubrid/cubrid_driver.php
@@ -0,0 +1,406 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 2.1.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CUBRID Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		Esen Sagynov
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_cubrid_driver extends CI_DB {
+
+	/**
+	 * Database driver
+	 *
+	 * @var	string
+	 */
+	public $dbdriver = 'cubrid';
+
+	/**
+	 * Auto-commit flag
+	 *
+	 * @var	bool
+	 */
+	public $auto_commit = TRUE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Identifier escape character
+	 *
+	 * @var	string
+	 */
+	protected $_escape_char = '`';
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('RANDOM()', 'RANDOM(%d)');
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if (preg_match('/^CUBRID:[^:]+(:[0-9][1-9]{0,4})?:[^:]+:[^:]*:[^:]*:(\?.+)?$/', $this->dsn, $matches))
+		{
+			if (stripos($matches[2], 'autocommit=off') !== FALSE)
+			{
+				$this->auto_commit = FALSE;
+			}
+		}
+		else
+		{
+			// If no port is defined by the user, use the default value
+			empty($this->port) OR $this->port = 33000;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Non-persistent database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	resource
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		if (preg_match('/^CUBRID:[^:]+(:[0-9][1-9]{0,4})?:[^:]+:([^:]*):([^:]*):(\?.+)?$/', $this->dsn, $matches))
+		{
+			$func = ($persistent !== TRUE) ? 'cubrid_connect_with_url' : 'cubrid_pconnect_with_url';
+			return ($matches[2] === '' && $matches[3] === '' && $this->username !== '' && $this->password !== '')
+				? $func($this->dsn, $this->username, $this->password)
+				: $func($this->dsn);
+		}
+
+		$func = ($persistent !== TRUE) ? 'cubrid_connect' : 'cubrid_pconnect';
+		return ($this->username !== '')
+			? $func($this->hostname, $this->port, $this->database, $this->username, $this->password)
+			: $func($this->hostname, $this->port, $this->database);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Reconnect
+	 *
+	 * Keep / reestablish the db connection if no queries have been
+	 * sent for a length of time exceeding the server's idle timeout
+	 *
+	 * @return	void
+	 */
+	public function reconnect()
+	{
+		if (cubrid_ping($this->conn_id) === FALSE)
+		{
+			$this->conn_id = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database version number
+	 *
+	 * @return	string
+	 */
+	public function version()
+	{
+		if (isset($this->data_cache['version']))
+		{
+			return $this->data_cache['version'];
+		}
+
+		return ( ! $this->conn_id OR ($version = cubrid_get_server_info($this->conn_id)) === FALSE)
+			? FALSE
+			: $this->data_cache['version'] = $version;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Execute the query
+	 *
+	 * @param	string	$sql	an SQL query
+	 * @return	resource
+	 */
+	protected function _execute($sql)
+	{
+		return cubrid_query($sql, $this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Begin Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_begin()
+	{
+		if (($autocommit = cubrid_get_autocommit($this->conn_id)) === NULL)
+		{
+			return FALSE;
+		}
+		elseif ($autocommit === TRUE)
+		{
+			return cubrid_set_autocommit($this->conn_id, CUBRID_AUTOCOMMIT_FALSE);
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Commit Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_commit()
+	{
+		if ( ! cubrid_commit($this->conn_id))
+		{
+			return FALSE;
+		}
+
+		if ($this->auto_commit && ! cubrid_get_autocommit($this->conn_id))
+		{
+			return cubrid_set_autocommit($this->conn_id, CUBRID_AUTOCOMMIT_TRUE);
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rollback Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_rollback()
+	{
+		if ( ! cubrid_rollback($this->conn_id))
+		{
+			return FALSE;
+		}
+
+		if ($this->auto_commit && ! cubrid_get_autocommit($this->conn_id))
+		{
+			cubrid_set_autocommit($this->conn_id, CUBRID_AUTOCOMMIT_TRUE);
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Platform-dependent string escape
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _escape_str($str)
+	{
+		return cubrid_real_escape_string($str, $this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Affected Rows
+	 *
+	 * @return	int
+	 */
+	public function affected_rows()
+	{
+		return cubrid_affected_rows();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert ID
+	 *
+	 * @return	int
+	 */
+	public function insert_id()
+	{
+		return cubrid_insert_id($this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * List table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SHOW TABLES';
+
+		if ($prefix_limit !== FALSE && $this->dbprefix !== '')
+		{
+			return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'";
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE)
+		{
+			return FALSE;
+		}
+		$query = $query->result_object();
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]->Field;
+
+			sscanf($query[$i]->Type, '%[a-z](%d)',
+				$retval[$i]->type,
+				$retval[$i]->max_length
+			);
+
+			$retval[$i]->default		= $query[$i]->Default;
+			$retval[$i]->primary_key	= (int) ($query[$i]->Key === 'PRI');
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error
+	 *
+	 * Returns an array containing code and message of the last
+	 * database error that has occurred.
+	 *
+	 * @return	array
+	 */
+	public function error()
+	{
+		return array('code' => cubrid_errno($this->conn_id), 'message' => cubrid_error($this->conn_id));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * FROM tables
+	 *
+	 * Groups tables in FROM clauses if needed, so there is no confusion
+	 * about operator precedence.
+	 *
+	 * @return	string
+	 */
+	protected function _from_tables()
+	{
+		if ( ! empty($this->qb_join) && count($this->qb_from) > 1)
+		{
+			return '('.implode(', ', $this->qb_from).')';
+		}
+
+		return implode(', ', $this->qb_from);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close DB Connection
+	 *
+	 * @return	void
+	 */
+	protected function _close()
+	{
+		cubrid_close($this->conn_id);
+	}
+
+}
diff --git a/system/database/drivers/cubrid/cubrid_forge.php b/system/database/drivers/cubrid/cubrid_forge.php
new file mode 100644
index 0000000..e8e201f
--- /dev/null
+++ b/system/database/drivers/cubrid/cubrid_forge.php
@@ -0,0 +1,231 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 2.1.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CUBRID Forge Class
+ *
+ * @category	Database
+ * @author		Esen Sagynov
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_cubrid_forge extends CI_DB_forge {
+
+	/**
+	 * CREATE DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_database	= FALSE;
+
+	/**
+	 * CREATE TABLE keys flag
+	 *
+	 * Whether table keys are created from within the
+	 * CREATE TABLE statement.
+	 *
+	 * @var	bool
+	 */
+	protected $_create_table_keys	= TRUE;
+
+	/**
+	 * DROP DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_database	= FALSE;
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= FALSE;
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'SHORT'		=> 'INTEGER',
+		'SMALLINT'	=> 'INTEGER',
+		'INT'		=> 'BIGINT',
+		'INTEGER'	=> 'BIGINT',
+		'BIGINT'	=> 'NUMERIC',
+		'FLOAT'		=> 'DOUBLE',
+		'REAL'		=> 'DOUBLE'
+	);
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if (in_array($alter_type, array('DROP', 'ADD'), TRUE))
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table);
+		$sqls = array();
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			if ($field[$i]['_literal'] !== FALSE)
+			{
+				$sqls[] = $sql.' CHANGE '.$field[$i]['_literal'];
+			}
+			else
+			{
+				$alter_type = empty($field[$i]['new_name']) ? ' MODIFY ' : ' CHANGE ';
+				$sqls[] = $sql.$alter_type.$this->_process_column($field[$i]);
+			}
+		}
+
+		return $sqls;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process column
+	 *
+	 * @param	array	$field
+	 * @return	string
+	 */
+	protected function _process_column($field)
+	{
+		$extra_clause = isset($field['after'])
+			? ' AFTER '.$this->db->escape_identifiers($field['after']) : '';
+
+		if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE)
+		{
+			$extra_clause = ' FIRST';
+		}
+
+		return $this->db->escape_identifiers($field['name'])
+			.(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name']))
+			.' '.$field['type'].$field['length']
+			.$field['unsigned']
+			.$field['null']
+			.$field['default']
+			.$field['auto_increment']
+			.$field['unique']
+			.$extra_clause;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'TINYINT':
+				$attributes['TYPE'] = 'SMALLINT';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'INTEGER';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'LONGTEXT':
+				$attributes['TYPE'] = 'STRING';
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process indexes
+	 *
+	 * @param	string	$table	(ignored)
+	 * @return	string
+	 */
+	protected function _process_indexes($table)
+	{
+		$sql = '';
+
+		for ($i = 0, $c = count($this->keys); $i < $c; $i++)
+		{
+			if (is_array($this->keys[$i]))
+			{
+				for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++)
+				{
+					if ( ! isset($this->fields[$this->keys[$i][$i2]]))
+					{
+						unset($this->keys[$i][$i2]);
+						continue;
+					}
+				}
+			}
+			elseif ( ! isset($this->fields[$this->keys[$i]]))
+			{
+				unset($this->keys[$i]);
+				continue;
+			}
+
+			is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]);
+
+			$sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i]))
+				.' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')';
+		}
+
+		$this->keys = array();
+
+		return $sql;
+	}
+
+}
diff --git a/system/database/drivers/cubrid/cubrid_result.php b/system/database/drivers/cubrid/cubrid_result.php
new file mode 100644
index 0000000..274b0c9
--- /dev/null
+++ b/system/database/drivers/cubrid/cubrid_result.php
@@ -0,0 +1,178 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 2.1.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CUBRID Result Class
+ *
+ * This class extends the parent result class: CI_DB_result
+ *
+ * @category	Database
+ * @author		Esen Sagynov
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_cubrid_result extends CI_DB_result {
+
+	/**
+	 * Number of rows in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_rows()
+	{
+		return is_int($this->num_rows)
+			? $this->num_rows
+			: $this->num_rows = cubrid_num_rows($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Number of fields in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_fields()
+	{
+		return cubrid_num_fields($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * Generates an array of column names
+	 *
+	 * @return	array
+	 */
+	public function list_fields()
+	{
+		return cubrid_column_names($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data
+	 *
+	 * Generates an array of objects containing field meta-data
+	 *
+	 * @return	array
+	 */
+	public function field_data()
+	{
+		$retval = array();
+
+		for ($i = 0, $c = $this->num_fields(); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= cubrid_field_name($this->result_id, $i);
+			$retval[$i]->type		= cubrid_field_type($this->result_id, $i);
+			$retval[$i]->max_length		= cubrid_field_len($this->result_id, $i);
+			$retval[$i]->primary_key	= (int) (strpos(cubrid_field_flags($this->result_id, $i), 'primary_key') !== FALSE);
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Free the result
+	 *
+	 * @return	void
+	 */
+	public function free_result()
+	{
+		if (is_resource($this->result_id) OR
+			(get_resource_type($this->result_id) === 'Unknown' && preg_match('/Resource id #/', strval($this->result_id))))
+		{
+			cubrid_close_request($this->result_id);
+			$this->result_id = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Data Seek
+	 *
+	 * Moves the internal pointer to the desired offset. We call
+	 * this internally before fetching results to make sure the
+	 * result set starts at zero.
+	 *
+	 * @param	int	$n
+	 * @return	bool
+	 */
+	public function data_seek($n = 0)
+	{
+		return cubrid_data_seek($this->result_id, $n);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - associative array
+	 *
+	 * Returns the result set as an array
+	 *
+	 * @return	array
+	 */
+	protected function _fetch_assoc()
+	{
+		return cubrid_fetch_assoc($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - object
+	 *
+	 * Returns the result set as an object
+	 *
+	 * @param	string	$class_name
+	 * @return	object
+	 */
+	protected function _fetch_object($class_name = 'stdClass')
+	{
+		return cubrid_fetch_object($this->result_id, $class_name);
+	}
+
+}
diff --git a/system/database/drivers/cubrid/cubrid_utility.php b/system/database/drivers/cubrid/cubrid_utility.php
new file mode 100644
index 0000000..ca81568
--- /dev/null
+++ b/system/database/drivers/cubrid/cubrid_utility.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 2.1.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CUBRID Utility Class
+ *
+ * @category	Database
+ * @author		Esen Sagynov
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_cubrid_utility extends CI_DB_utility {
+
+	/**
+	 * List databases
+	 *
+	 * @return	array
+	 */
+	public function list_databases()
+	{
+		if (isset($this->db->data_cache['db_names']))
+		{
+			return $this->db->data_cache['db_names'];
+		}
+
+		return $this->db->data_cache['db_names'] = cubrid_list_dbs($this->db->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * CUBRID Export
+	 *
+	 * @param	array	Preferences
+	 * @return	mixed
+	 */
+	protected function _backup($params = array())
+	{
+		// No SQL based support in CUBRID as of version 8.4.0. Database or
+		// table backup can be performed using CUBRID Manager
+		// database administration tool.
+		return $this->db->display_error('db_unsupported_feature');
+	}
+}
diff --git a/system/database/drivers/cubrid/index.html b/system/database/drivers/cubrid/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/drivers/cubrid/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/system/database/drivers/ibase/ibase_driver.php b/system/database/drivers/ibase/ibase_driver.php
new file mode 100644
index 0000000..433139f
--- /dev/null
+++ b/system/database/drivers/ibase/ibase_driver.php
@@ -0,0 +1,414 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Firebird/Interbase Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_ibase_driver extends CI_DB {
+
+	/**
+	 * Database driver
+	 *
+	 * @var	string
+	 */
+	public $dbdriver = 'ibase';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('RAND()', 'RAND()');
+
+	/**
+	 * IBase Transaction status flag
+	 *
+	 * @var	resource
+	 */
+	protected $_ibase_trans;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Non-persistent database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	resource
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		return ($persistent === TRUE)
+			? ibase_pconnect($this->hostname.':'.$this->database, $this->username, $this->password, $this->char_set)
+			: ibase_connect($this->hostname.':'.$this->database, $this->username, $this->password, $this->char_set);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database version number
+	 *
+	 * @return	string
+	 */
+	public function version()
+	{
+		if (isset($this->data_cache['version']))
+		{
+			return $this->data_cache['version'];
+		}
+
+		if (($service = ibase_service_attach($this->hostname, $this->username, $this->password)))
+		{
+			$this->data_cache['version'] = ibase_server_info($service, IBASE_SVC_SERVER_VERSION);
+
+			// Don't keep the service open
+			ibase_service_detach($service);
+			return $this->data_cache['version'];
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Execute the query
+	 *
+	 * @param	string	$sql	an SQL query
+	 * @return	resource
+	 */
+	protected function _execute($sql)
+	{
+		return ibase_query(isset($this->_ibase_trans) ? $this->_ibase_trans : $this->conn_id, $sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Begin Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_begin()
+	{
+		if (($trans_handle = ibase_trans($this->conn_id)) === FALSE)
+		{
+			return FALSE;
+		}
+
+		$this->_ibase_trans = $trans_handle;
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Commit Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_commit()
+	{
+		if (ibase_commit($this->_ibase_trans))
+		{
+			$this->_ibase_trans = NULL;
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rollback Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_rollback()
+	{
+		if (ibase_rollback($this->_ibase_trans))
+		{
+			$this->_ibase_trans = NULL;
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Affected Rows
+	 *
+	 * @return	int
+	 */
+	public function affected_rows()
+	{
+		return ibase_affected_rows($this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert ID
+	 *
+	 * @param	string	$generator_name
+	 * @param	int	$inc_by
+	 * @return	int
+	 */
+	public function insert_id($generator_name, $inc_by = 0)
+	{
+		//If a generator hasn't been used before it will return 0
+		return ibase_gen_id('"'.$generator_name.'"', $inc_by);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * List table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SELECT TRIM("RDB$RELATION_NAME") AS TABLE_NAME FROM "RDB$RELATIONS" WHERE "RDB$RELATION_NAME" NOT LIKE \'RDB$%\' AND "RDB$RELATION_NAME" NOT LIKE \'MON$%\'';
+
+		if ($prefix_limit !== FALSE && $this->dbprefix !== '')
+		{
+			return $sql.' AND TRIM("RDB$RELATION_NAME") AS TABLE_NAME LIKE \''.$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SELECT TRIM("RDB$FIELD_NAME") AS COLUMN_NAME FROM "RDB$RELATION_FIELDS" WHERE "RDB$RELATION_NAME" = '.$this->escape($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		$sql = 'SELECT "rfields"."RDB$FIELD_NAME" AS "name",
+				CASE "fields"."RDB$FIELD_TYPE"
+					WHEN 7 THEN \'SMALLINT\'
+					WHEN 8 THEN \'INTEGER\'
+					WHEN 9 THEN \'QUAD\'
+					WHEN 10 THEN \'FLOAT\'
+					WHEN 11 THEN \'DFLOAT\'
+					WHEN 12 THEN \'DATE\'
+					WHEN 13 THEN \'TIME\'
+					WHEN 14 THEN \'CHAR\'
+					WHEN 16 THEN \'INT64\'
+					WHEN 27 THEN \'DOUBLE\'
+					WHEN 35 THEN \'TIMESTAMP\'
+					WHEN 37 THEN \'VARCHAR\'
+					WHEN 40 THEN \'CSTRING\'
+					WHEN 261 THEN \'BLOB\'
+					ELSE NULL
+				END AS "type",
+				"fields"."RDB$FIELD_LENGTH" AS "max_length",
+				"rfields"."RDB$DEFAULT_VALUE" AS "default"
+			FROM "RDB$RELATION_FIELDS" "rfields"
+				JOIN "RDB$FIELDS" "fields" ON "rfields"."RDB$FIELD_SOURCE" = "fields"."RDB$FIELD_NAME"
+			WHERE "rfields"."RDB$RELATION_NAME" = '.$this->escape($table).'
+			ORDER BY "rfields"."RDB$FIELD_POSITION"';
+
+		return (($query = $this->query($sql)) !== FALSE)
+			? $query->result_object()
+			: FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error
+	 *
+	 * Returns an array containing code and message of the last
+	 * database error that has occurred.
+	 *
+	 * @return	array
+	 */
+	public function error()
+	{
+		return array('code' => ibase_errcode(), 'message' => ibase_errmsg());
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update statement
+	 *
+	 * Generates a platform-specific update string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @param	array	$values
+	 * @return	string
+	 */
+	protected function _update($table, $values)
+	{
+		$this->qb_limit = FALSE;
+		return parent::_update($table, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Truncate statement
+	 *
+	 * Generates a platform-specific truncate string from the supplied data
+	 *
+	 * If the database does not support the TRUNCATE statement,
+	 * then this method maps to 'DELETE FROM table'
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _truncate($table)
+	{
+		return 'DELETE FROM '.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete statement
+	 *
+	 * Generates a platform-specific delete string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _delete($table)
+	{
+		$this->qb_limit = FALSE;
+		return parent::_delete($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT
+	 *
+	 * Generates a platform-specific LIMIT clause
+	 *
+	 * @param	string	$sql	SQL Query
+	 * @return	string
+	 */
+	protected function _limit($sql)
+	{
+		// Limit clause depends on if Interbase or Firebird
+		if (stripos($this->version(), 'firebird') !== FALSE)
+		{
+			$select = 'FIRST '.$this->qb_limit
+				.($this->qb_offset ? ' SKIP '.$this->qb_offset : '');
+		}
+		else
+		{
+			$select = 'ROWS '
+				.($this->qb_offset ? $this->qb_offset.' TO '.($this->qb_limit + $this->qb_offset) : $this->qb_limit);
+		}
+
+		return preg_replace('`SELECT`i', 'SELECT '.$select, $sql, 1);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert batch statement
+	 *
+	 * Generates a platform-specific insert string from the supplied data.
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$keys	INSERT keys
+	 * @param	array	$values	INSERT values
+	 * @return	string|bool
+	 */
+	protected function _insert_batch($table, $keys, $values)
+	{
+		return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close DB Connection
+	 *
+	 * @return	void
+	 */
+	protected function _close()
+	{
+		ibase_close($this->conn_id);
+	}
+
+}
diff --git a/system/database/drivers/ibase/ibase_forge.php b/system/database/drivers/ibase/ibase_forge.php
new file mode 100644
index 0000000..2c385f1
--- /dev/null
+++ b/system/database/drivers/ibase/ibase_forge.php
@@ -0,0 +1,252 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Interbase/Firebird Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_ibase_forge extends CI_DB_forge {
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= FALSE;
+
+	/**
+	 * RENAME TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_rename_table	= FALSE;
+
+	/**
+	 * DROP TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_table_if	= FALSE;
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'SMALLINT'	=> 'INTEGER',
+		'INTEGER'	=> 'INT64',
+		'FLOAT'		=> 'DOUBLE PRECISION'
+	);
+
+	/**
+	 * NULL value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_null		= 'NULL';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Create database
+	 *
+	 * @param	string	$db_name
+	 * @return	bool
+	 */
+	public function create_database($db_name)
+	{
+		// Firebird databases are flat files, so a path is required
+
+		// Hostname is needed for remote access
+		empty($this->db->hostname) OR $db_name = $this->hostname.':'.$db_name;
+
+		return parent::create_database('"'.$db_name.'"');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Drop database
+	 *
+	 * @param	string	$db_name	(ignored)
+	 * @return	bool
+	 */
+	public function drop_database($db_name)
+	{
+		if ( ! ibase_drop_db($this->conn_id))
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE;
+		}
+		elseif ( ! empty($this->db->data_cache['db_names']))
+		{
+			$key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE);
+			if ($key !== FALSE)
+			{
+				unset($this->db->data_cache['db_names'][$key]);
+			}
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if (in_array($alter_type, array('DROP', 'ADD'), TRUE))
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table);
+		$sqls = array();
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			if ($field[$i]['_literal'] !== FALSE)
+			{
+				return FALSE;
+			}
+
+			if (isset($field[$i]['type']))
+			{
+				$sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identififers($field[$i]['name'])
+					.' TYPE '.$field[$i]['type'].$field[$i]['length'];
+			}
+
+			if ( ! empty($field[$i]['default']))
+			{
+				$sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+					.' SET DEFAULT '.$field[$i]['default'];
+			}
+
+			if (isset($field[$i]['null']))
+			{
+				$sqls[] = 'UPDATE "RDB$RELATION_FIELDS" SET "RDB$NULL_FLAG" = '
+					.($field[$i]['null'] === TRUE ? 'NULL' : '1')
+					.' WHERE "RDB$FIELD_NAME" = '.$this->db->escape($field[$i]['name'])
+					.' AND "RDB$RELATION_NAME" = '.$this->db->escape($table);
+			}
+
+			if ( ! empty($field[$i]['new_name']))
+			{
+				$sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+					.' TO '.$this->db->escape_identifiers($field[$i]['new_name']);
+			}
+		}
+
+		return $sqls;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process column
+	 *
+	 * @param	array	$field
+	 * @return	string
+	 */
+	protected function _process_column($field)
+	{
+		return $this->db->escape_identifiers($field['name'])
+			.' '.$field['type'].$field['length']
+			.$field['null']
+			.$field['unique']
+			.$field['default'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'TINYINT':
+				$attributes['TYPE'] = 'SMALLINT';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'INTEGER';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'INT':
+				$attributes['TYPE'] = 'INTEGER';
+				return;
+			case 'BIGINT':
+				$attributes['TYPE'] = 'INT64';
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		// Not supported
+	}
+
+}
diff --git a/system/database/drivers/ibase/ibase_result.php b/system/database/drivers/ibase/ibase_result.php
new file mode 100644
index 0000000..900212e
--- /dev/null
+++ b/system/database/drivers/ibase/ibase_result.php
@@ -0,0 +1,162 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Interbase/Firebird Result Class
+ *
+ * This class extends the parent result class: CI_DB_result
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_ibase_result extends CI_DB_result {
+
+	/**
+	 * Number of fields in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_fields()
+	{
+		return ibase_num_fields($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * Generates an array of column names
+	 *
+	 * @return	array
+	 */
+	public function list_fields()
+	{
+		$field_names = array();
+		for ($i = 0, $num_fields = $this->num_fields(); $i < $num_fields; $i++)
+		{
+			$info = ibase_field_info($this->result_id, $i);
+			$field_names[] = $info['name'];
+		}
+
+		return $field_names;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data
+	 *
+	 * Generates an array of objects containing field meta-data
+	 *
+	 * @return	array
+	 */
+	public function field_data()
+	{
+		$retval = array();
+		for ($i = 0, $c = $this->num_fields(); $i < $c; $i++)
+		{
+			$info = ibase_field_info($this->result_id, $i);
+
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $info['name'];
+			$retval[$i]->type		= $info['type'];
+			$retval[$i]->max_length		= $info['length'];
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Free the result
+	 *
+	 * @return	void
+	 */
+	public function free_result()
+	{
+		ibase_free_result($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - associative array
+	 *
+	 * Returns the result set as an array
+	 *
+	 * @return	array
+	 */
+	protected function _fetch_assoc()
+	{
+		return ibase_fetch_assoc($this->result_id, IBASE_FETCH_BLOBS);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - object
+	 *
+	 * Returns the result set as an object
+	 *
+	 * @param	string	$class_name
+	 * @return	object
+	 */
+	protected function _fetch_object($class_name = 'stdClass')
+	{
+		$row = ibase_fetch_object($this->result_id, IBASE_FETCH_BLOBS);
+
+		if ($class_name === 'stdClass' OR ! $row)
+		{
+			return $row;
+		}
+
+		$class_name = new $class_name();
+		foreach ($row as $key => $value)
+		{
+			$class_name->$key = $value;
+		}
+
+		return $class_name;
+	}
+
+}
diff --git a/system/database/drivers/ibase/ibase_utility.php b/system/database/drivers/ibase/ibase_utility.php
new file mode 100644
index 0000000..bc87508
--- /dev/null
+++ b/system/database/drivers/ibase/ibase_utility.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Interbase/Firebird Utility Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_ibase_utility extends CI_DB_utility {
+
+	/**
+	 * Export
+	 *
+	 * @param	string	$filename
+	 * @return	mixed
+	 */
+	protected function _backup($filename)
+	{
+		if ($service = ibase_service_attach($this->db->hostname, $this->db->username, $this->db->password))
+		{
+			$res = ibase_backup($service, $this->db->database, $filename.'.fbk');
+
+			// Close the service connection
+			ibase_service_detach($service);
+			return $res;
+		}
+
+		return FALSE;
+	}
+
+}
diff --git a/system/database/drivers/ibase/index.html b/system/database/drivers/ibase/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/drivers/ibase/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/system/database/drivers/index.html b/system/database/drivers/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/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/system/database/drivers/mssql/index.html b/system/database/drivers/mssql/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/drivers/mssql/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/system/database/drivers/mssql/mssql_driver.php b/system/database/drivers/mssql/mssql_driver.php
new file mode 100644
index 0000000..5012640
--- /dev/null
+++ b/system/database/drivers/mssql/mssql_driver.php
@@ -0,0 +1,519 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * MS SQL Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_mssql_driver extends CI_DB {
+
+	/**
+	 * Database driver
+	 *
+	 * @var	string
+	 */
+	public $dbdriver = 'mssql';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('NEWID()', 'RAND(%d)');
+
+	/**
+	 * Quoted identifier flag
+	 *
+	 * Whether to use SQL-92 standard quoted identifier
+	 * (double quotes) or brackets for identifier escaping.
+	 *
+	 * @var	bool
+	 */
+	protected $_quoted_identifier = TRUE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Appends the port number to the hostname, if needed.
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if ( ! empty($this->port))
+		{
+			$this->hostname .= (DIRECTORY_SEPARATOR === '\\' ? ',' : ':').$this->port;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Non-persistent database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	resource
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		$this->conn_id = ($persistent)
+				? mssql_pconnect($this->hostname, $this->username, $this->password)
+				: mssql_connect($this->hostname, $this->username, $this->password);
+
+		if ( ! $this->conn_id)
+		{
+			return FALSE;
+		}
+
+		// ----------------------------------------------------------------
+
+		// Select the DB... assuming a database name is specified in the config file
+		if ($this->database !== '' && ! $this->db_select())
+		{
+			log_message('error', 'Unable to select database: '.$this->database);
+
+			return ($this->db_debug === TRUE)
+				? $this->display_error('db_unable_to_select', $this->database)
+				: FALSE;
+		}
+
+		// Determine how identifiers are escaped
+		$query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi');
+		$query = $query->row_array();
+		$this->_quoted_identifier = empty($query) ? FALSE : (bool) $query['qi'];
+		$this->_escape_char = ($this->_quoted_identifier) ? '"' : array('[', ']');
+
+		return $this->conn_id;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Select the database
+	 *
+	 * @param	string	$database
+	 * @return	bool
+	 */
+	public function db_select($database = '')
+	{
+		if ($database === '')
+		{
+			$database = $this->database;
+		}
+
+		// Note: Escaping is required in the event that the DB name
+		// contains reserved characters.
+		if (mssql_select_db('['.$database.']', $this->conn_id))
+		{
+			$this->database = $database;
+			$this->data_cache = array();
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Execute the query
+	 *
+	 * @param	string	$sql	an SQL query
+	 * @return	mixed	resource if rows are returned, bool otherwise
+	 */
+	protected function _execute($sql)
+	{
+		return mssql_query($sql, $this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Begin Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_begin()
+	{
+		return $this->simple_query('BEGIN TRAN');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Commit Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_commit()
+	{
+		return $this->simple_query('COMMIT TRAN');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rollback Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_rollback()
+	{
+		return $this->simple_query('ROLLBACK TRAN');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Affected Rows
+	 *
+	 * @return	int
+	 */
+	public function affected_rows()
+	{
+		return mssql_rows_affected($this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert ID
+	 *
+	 * Returns the last id created in the Identity column.
+	 *
+	 * @return	string
+	 */
+	public function insert_id()
+	{
+		$query = version_compare($this->version(), '8', '>=')
+			? 'SELECT SCOPE_IDENTITY() AS last_id'
+			: 'SELECT @@IDENTITY AS last_id';
+
+		$query = $this->query($query);
+		$query = $query->row();
+		return $query->last_id;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set client character set
+	 *
+	 * @param	string	$charset
+	 * @return	bool
+	 */
+	protected function _db_set_charset($charset)
+	{
+		return (ini_set('mssql.charset', $charset) !== FALSE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Version number query string
+	 *
+	 * @return	string
+	 */
+	protected function _version()
+	{
+		return "SELECT SERVERPROPERTY('ProductVersion') AS ver";
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * List table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SELECT '.$this->escape_identifiers('name')
+			.' FROM '.$this->escape_identifiers('sysobjects')
+			.' WHERE '.$this->escape_identifiers('type')." = 'U'";
+
+		if ($prefix_limit !== FALSE && $this->dbprefix !== '')
+		{
+			$sql .= ' AND '.$this->escape_identifiers('name')." LIKE '".$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql.' ORDER BY '.$this->escape_identifiers('name');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * List column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SELECT COLUMN_NAME
+			FROM INFORMATION_SCHEMA.Columns
+			WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		$sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT
+			FROM INFORMATION_SCHEMA.Columns
+			WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
+
+		if (($query = $this->query($sql)) === FALSE)
+		{
+			return FALSE;
+		}
+		$query = $query->result_object();
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]->COLUMN_NAME;
+			$retval[$i]->type		= $query[$i]->DATA_TYPE;
+			$retval[$i]->max_length		= ($query[$i]->CHARACTER_MAXIMUM_LENGTH > 0) ? $query[$i]->CHARACTER_MAXIMUM_LENGTH : $query[$i]->NUMERIC_PRECISION;
+			$retval[$i]->default		= $query[$i]->COLUMN_DEFAULT;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error
+	 *
+	 * Returns an array containing code and message of the last
+	 * database error that has occurred.
+	 *
+	 * @return	array
+	 */
+	public function error()
+	{
+		// We need this because the error info is discarded by the
+		// server the first time you request it, and query() already
+		// calls error() once for logging purposes when a query fails.
+		static $error = array('code' => 0, 'message' => NULL);
+
+		$message = mssql_get_last_message();
+		if ( ! empty($message))
+		{
+			$error['code']    = $this->query('SELECT @@ERROR AS code')->row()->code;
+			$error['message'] = $message;
+		}
+
+		return $error;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update statement
+	 *
+	 * Generates a platform-specific update string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @param	array	$values
+	 * @return	string
+	 */
+	protected function _update($table, $values)
+	{
+		$this->qb_limit = FALSE;
+		$this->qb_orderby = array();
+		return parent::_update($table, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Truncate statement
+	 *
+	 * Generates a platform-specific truncate string from the supplied data
+	 *
+	 * If the database does not support the TRUNCATE statement,
+	 * then this method maps to 'DELETE FROM table'
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _truncate($table)
+	{
+		return 'TRUNCATE TABLE '.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete statement
+	 *
+	 * Generates a platform-specific delete string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _delete($table)
+	{
+		if ($this->qb_limit)
+		{
+			return 'WITH ci_delete AS (SELECT TOP '.$this->qb_limit.' * FROM '.$table.$this->_compile_wh('qb_where').') DELETE FROM ci_delete';
+		}
+
+		return parent::_delete($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT
+	 *
+	 * Generates a platform-specific LIMIT clause
+	 *
+	 * @param	string	$sql	SQL Query
+	 * @return	string
+	 */
+	protected function _limit($sql)
+	{
+		$limit = $this->qb_offset + $this->qb_limit;
+
+		// As of SQL Server 2005 (9.0.*) ROW_NUMBER() is supported,
+		// however an ORDER BY clause is required for it to work
+		if (version_compare($this->version(), '9', '>=') && $this->qb_offset && ! empty($this->qb_orderby))
+		{
+			$orderby = $this->_compile_order_by();
+
+			// We have to strip the ORDER BY clause
+			$sql = trim(substr($sql, 0, strrpos($sql, $orderby)));
+
+			// Get the fields to select from our subquery, so that we can avoid CI_rownum appearing in the actual results
+			if (count($this->qb_select) === 0 OR strpos(implode(',', $this->qb_select), '*') !== FALSE)
+			{
+				$select = '*'; // Inevitable
+			}
+			else
+			{
+				// Use only field names and their aliases, everything else is out of our scope.
+				$select = array();
+				$field_regexp = ($this->_quoted_identifier)
+					? '("[^\"]+")' : '(\[[^\]]+\])';
+				for ($i = 0, $c = count($this->qb_select); $i < $c; $i++)
+				{
+					$select[] = preg_match('/(?:\s|\.)'.$field_regexp.'$/i', $this->qb_select[$i], $m)
+						? $m[1] : $this->qb_select[$i];
+				}
+				$select = implode(', ', $select);
+			}
+
+			return 'SELECT '.$select." FROM (\n\n"
+				.preg_replace('/^(SELECT( DISTINCT)?)/i', '\\1 ROW_NUMBER() OVER('.trim($orderby).') AS '.$this->escape_identifiers('CI_rownum').', ', $sql)
+				."\n\n) ".$this->escape_identifiers('CI_subquery')
+				."\nWHERE ".$this->escape_identifiers('CI_rownum').' BETWEEN '.($this->qb_offset + 1).' AND '.$limit;
+		}
+
+		return preg_replace('/(^\SELECT (DISTINCT)?)/i','\\1 TOP '.$limit.' ', $sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert batch statement
+	 *
+	 * Generates a platform-specific insert string from the supplied data.
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$keys	INSERT keys
+	 * @param	array	$values	INSERT values
+	 * @return	string|bool
+	 */
+	protected function _insert_batch($table, $keys, $values)
+	{
+		// Multiple-value inserts are only supported as of SQL Server 2008
+		if (version_compare($this->version(), '10', '>='))
+		{
+			return parent::_insert_batch($table, $keys, $values);
+		}
+
+		return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close DB Connection
+	 *
+	 * @return	void
+	 */
+	protected function _close()
+	{
+		mssql_close($this->conn_id);
+	}
+
+}
diff --git a/system/database/drivers/mssql/mssql_forge.php b/system/database/drivers/mssql/mssql_forge.php
new file mode 100644
index 0000000..f9dee91
--- /dev/null
+++ b/system/database/drivers/mssql/mssql_forge.php
@@ -0,0 +1,152 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * MS SQL Forge Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_mssql_forge extends CI_DB_forge {
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= "IF NOT EXISTS (SELECT * FROM sysobjects WHERE ID = object_id(N'%s') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)\nCREATE TABLE";
+
+	/**
+	 * DROP TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_table_if	= "IF EXISTS (SELECT * FROM sysobjects WHERE ID = object_id(N'%s') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)\nDROP TABLE";
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'TINYINT'	=> 'SMALLINT',
+		'SMALLINT'	=> 'INT',
+		'INT'		=> 'BIGINT',
+		'REAL'		=> 'FLOAT'
+	);
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if (in_array($alter_type, array('ADD', 'DROP'), TRUE))
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' ALTER COLUMN ';
+		$sqls = array();
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			$sqls[] = $sql.$this->_process_column($field[$i]);
+		}
+
+		return $sqls;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		if (isset($attributes['CONSTRAINT']) && strpos($attributes['TYPE'], 'INT') !== FALSE)
+		{
+			unset($attributes['CONSTRAINT']);
+		}
+
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'INTEGER';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'INTEGER':
+				$attributes['TYPE'] = 'INT';
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE)
+		{
+			$field['auto_increment'] = ' IDENTITY(1,1)';
+		}
+	}
+
+}
diff --git a/system/database/drivers/mssql/mssql_result.php b/system/database/drivers/mssql/mssql_result.php
new file mode 100644
index 0000000..fbe2eb1
--- /dev/null
+++ b/system/database/drivers/mssql/mssql_result.php
@@ -0,0 +1,199 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * MSSQL Result Class
+ *
+ * This class extends the parent result class: CI_DB_result
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_mssql_result extends CI_DB_result {
+
+	/**
+	 * Number of rows in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_rows()
+	{
+		return is_int($this->num_rows)
+			? $this->num_rows
+			: $this->num_rows = mssql_num_rows($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Number of fields in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_fields()
+	{
+		return mssql_num_fields($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * Generates an array of column names
+	 *
+	 * @return	array
+	 */
+	public function list_fields()
+	{
+		$field_names = array();
+		mssql_field_seek($this->result_id, 0);
+		while ($field = mssql_fetch_field($this->result_id))
+		{
+			$field_names[] = $field->name;
+		}
+
+		return $field_names;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data
+	 *
+	 * Generates an array of objects containing field meta-data
+	 *
+	 * @return	array
+	 */
+	public function field_data()
+	{
+		$retval = array();
+		for ($i = 0, $c = $this->num_fields(); $i < $c; $i++)
+		{
+			$field = mssql_fetch_field($this->result_id, $i);
+
+			$retval[$i]		= new stdClass();
+			$retval[$i]->name	= $field->name;
+			$retval[$i]->type	= $field->type;
+			$retval[$i]->max_length	= $field->max_length;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Free the result
+	 *
+	 * @return	void
+	 */
+	public function free_result()
+	{
+		if (is_resource($this->result_id))
+		{
+			mssql_free_result($this->result_id);
+			$this->result_id = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Data Seek
+	 *
+	 * Moves the internal pointer to the desired offset. We call
+	 * this internally before fetching results to make sure the
+	 * result set starts at zero.
+	 *
+	 * @param	int	$n
+	 * @return	bool
+	 */
+	public function data_seek($n = 0)
+	{
+		return mssql_data_seek($this->result_id, $n);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - associative array
+	 *
+	 * Returns the result set as an array
+	 *
+	 * @return	array
+	 */
+	protected function _fetch_assoc()
+	{
+		return mssql_fetch_assoc($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - object
+	 *
+	 * Returns the result set as an object
+	 *
+	 * @param	string	$class_name
+	 * @return	object
+	 */
+	protected function _fetch_object($class_name = 'stdClass')
+	{
+		$row = mssql_fetch_object($this->result_id);
+
+		if ($class_name === 'stdClass' OR ! $row)
+		{
+			return $row;
+		}
+
+		$class_name = new $class_name();
+		foreach ($row as $key => $value)
+		{
+			$class_name->$key = $value;
+		}
+
+		return $class_name;
+	}
+
+}
diff --git a/system/database/drivers/mssql/mssql_utility.php b/system/database/drivers/mssql/mssql_utility.php
new file mode 100644
index 0000000..a739dc8
--- /dev/null
+++ b/system/database/drivers/mssql/mssql_utility.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * MS SQL Utility Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_mssql_utility extends CI_DB_utility {
+
+	/**
+	 * List databases statement
+	 *
+	 * @var	string
+	 */
+	protected $_list_databases	= 'EXEC sp_helpdb'; // Can also be: EXEC sp_databases
+
+	/**
+	 * OPTIMIZE TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_optimize_table	= 'ALTER INDEX all ON %s REORGANIZE';
+
+	/**
+	 * Export
+	 *
+	 * @param	array	$params	Preferences
+	 * @return	bool
+	 */
+	protected function _backup($params = array())
+	{
+		// Currently unsupported
+		return $this->db->display_error('db_unsupported_feature');
+	}
+
+}
diff --git a/system/database/drivers/mysql/index.html b/system/database/drivers/mysql/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/drivers/mysql/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/system/database/drivers/mysql/mysql_driver.php b/system/database/drivers/mysql/mysql_driver.php
new file mode 100644
index 0000000..367f89a
--- /dev/null
+++ b/system/database/drivers/mysql/mysql_driver.php
@@ -0,0 +1,495 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * MySQL Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_mysql_driver extends CI_DB {
+
+	/**
+	 * Database driver
+	 *
+	 * @var	string
+	 */
+	public $dbdriver = 'mysql';
+
+	/**
+	 * Compression flag
+	 *
+	 * @var	bool
+	 */
+	public $compress = FALSE;
+
+	/**
+	 * DELETE hack flag
+	 *
+	 * Whether to use the MySQL "delete hack" which allows the number
+	 * of affected rows to be shown. Uses a preg_replace when enabled,
+	 * adding a bit more processing to all queries.
+	 *
+	 * @var	bool
+	 */
+	public $delete_hack = TRUE;
+
+	/**
+	 * Strict ON flag
+	 *
+	 * Whether we're running in strict SQL mode.
+	 *
+	 * @var	bool
+	 */
+	public $stricton;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Identifier escape character
+	 *
+	 * @var	string
+	 */
+	protected $_escape_char = '`';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if ( ! empty($this->port))
+		{
+			$this->hostname .= ':'.$this->port;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Non-persistent database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	resource
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		$client_flags = ($this->compress === FALSE) ? 0 : MYSQL_CLIENT_COMPRESS;
+
+		if ($this->encrypt === TRUE)
+		{
+			$client_flags = $client_flags | MYSQL_CLIENT_SSL;
+		}
+
+		// Error suppression is necessary mostly due to PHP 5.5+ issuing E_DEPRECATED messages
+		$this->conn_id = ($persistent === TRUE)
+			? mysql_pconnect($this->hostname, $this->username, $this->password, $client_flags)
+			: mysql_connect($this->hostname, $this->username, $this->password, TRUE, $client_flags);
+
+		// ----------------------------------------------------------------
+
+		// Select the DB... assuming a database name is specified in the config file
+		if ($this->database !== '' && ! $this->db_select())
+		{
+			log_message('error', 'Unable to select database: '.$this->database);
+
+			return ($this->db_debug === TRUE)
+				? $this->display_error('db_unable_to_select', $this->database)
+				: FALSE;
+		}
+
+		if (isset($this->stricton) && is_resource($this->conn_id))
+		{
+			if ($this->stricton)
+			{
+				$this->simple_query('SET SESSION sql_mode = CONCAT(@@sql_mode, ",", "STRICT_ALL_TABLES")');
+			}
+			else
+			{
+				$this->simple_query(
+					'SET SESSION sql_mode =
+					REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
+					@@sql_mode,
+					"STRICT_ALL_TABLES,", ""),
+					",STRICT_ALL_TABLES", ""),
+					"STRICT_ALL_TABLES", ""),
+					"STRICT_TRANS_TABLES,", ""),
+					",STRICT_TRANS_TABLES", ""),
+					"STRICT_TRANS_TABLES", "")'
+				);
+			}
+		}
+
+		return $this->conn_id;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Reconnect
+	 *
+	 * Keep / reestablish the db connection if no queries have been
+	 * sent for a length of time exceeding the server's idle timeout
+	 *
+	 * @return	void
+	 */
+	public function reconnect()
+	{
+		if (mysql_ping($this->conn_id) === FALSE)
+		{
+			$this->conn_id = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Select the database
+	 *
+	 * @param	string	$database
+	 * @return	bool
+	 */
+	public function db_select($database = '')
+	{
+		if ($database === '')
+		{
+			$database = $this->database;
+		}
+
+		if (mysql_select_db($database, $this->conn_id))
+		{
+			$this->database = $database;
+			$this->data_cache = array();
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set client character set
+	 *
+	 * @param	string	$charset
+	 * @return	bool
+	 */
+	protected function _db_set_charset($charset)
+	{
+		return mysql_set_charset($charset, $this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database version number
+	 *
+	 * @return	string
+	 */
+	public function version()
+	{
+		if (isset($this->data_cache['version']))
+		{
+			return $this->data_cache['version'];
+		}
+
+		if ( ! $this->conn_id OR ($version = mysql_get_server_info($this->conn_id)) === FALSE)
+		{
+			return FALSE;
+		}
+
+		return $this->data_cache['version'] = $version;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Execute the query
+	 *
+	 * @param	string	$sql	an SQL query
+	 * @return	mixed
+	 */
+	protected function _execute($sql)
+	{
+		return mysql_query($this->_prep_query($sql), $this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Prep the query
+	 *
+	 * If needed, each database adapter can prep the query string
+	 *
+	 * @param	string	$sql	an SQL query
+	 * @return	string
+	 */
+	protected function _prep_query($sql)
+	{
+		// mysql_affected_rows() returns 0 for "DELETE FROM TABLE" queries. This hack
+		// modifies the query so that it a proper number of affected rows is returned.
+		if ($this->delete_hack === TRUE && preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql))
+		{
+			return trim($sql).' WHERE 1=1';
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Begin Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_begin()
+	{
+		$this->simple_query('SET AUTOCOMMIT=0');
+		return $this->simple_query('START TRANSACTION'); // can also be BEGIN or BEGIN WORK
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Commit Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_commit()
+	{
+		if ($this->simple_query('COMMIT'))
+		{
+			$this->simple_query('SET AUTOCOMMIT=1');
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rollback Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_rollback()
+	{
+		if ($this->simple_query('ROLLBACK'))
+		{
+			$this->simple_query('SET AUTOCOMMIT=1');
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Platform-dependent string escape
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _escape_str($str)
+	{
+		return mysql_real_escape_string($str, $this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Affected Rows
+	 *
+	 * @return	int
+	 */
+	public function affected_rows()
+	{
+		return mysql_affected_rows($this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert ID
+	 *
+	 * @return	int
+	 */
+	public function insert_id()
+	{
+		return mysql_insert_id($this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * List table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SHOW TABLES FROM '.$this->_escape_char.$this->database.$this->_escape_char;
+
+		if ($prefix_limit !== FALSE && $this->dbprefix !== '')
+		{
+			return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'";
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE)
+		{
+			return FALSE;
+		}
+		$query = $query->result_object();
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]->Field;
+
+			sscanf($query[$i]->Type, '%[a-z](%d)',
+				$retval[$i]->type,
+				$retval[$i]->max_length
+			);
+
+			$retval[$i]->default		= $query[$i]->Default;
+			$retval[$i]->primary_key	= (int) ($query[$i]->Key === 'PRI');
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error
+	 *
+	 * Returns an array containing code and message of the last
+	 * database error that has occurred.
+	 *
+	 * @return	array
+	 */
+	public function error()
+	{
+		return array('code' => mysql_errno($this->conn_id), 'message' => mysql_error($this->conn_id));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * FROM tables
+	 *
+	 * Groups tables in FROM clauses if needed, so there is no confusion
+	 * about operator precedence.
+	 *
+	 * @return	string
+	 */
+	protected function _from_tables()
+	{
+		if ( ! empty($this->qb_join) && count($this->qb_from) > 1)
+		{
+			return '('.implode(', ', $this->qb_from).')';
+		}
+
+		return implode(', ', $this->qb_from);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close DB Connection
+	 *
+	 * @return	void
+	 */
+	protected function _close()
+	{
+		// Error suppression to avoid annoying E_WARNINGs in cases
+		// where the connection has already been closed for some reason.
+		@mysql_close($this->conn_id);
+	}
+
+}
diff --git a/system/database/drivers/mysql/mysql_forge.php b/system/database/drivers/mysql/mysql_forge.php
new file mode 100644
index 0000000..410ea2d
--- /dev/null
+++ b/system/database/drivers/mysql/mysql_forge.php
@@ -0,0 +1,243 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * MySQL Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_mysql_forge extends CI_DB_forge {
+
+	/**
+	 * CREATE DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_database	= 'CREATE DATABASE %s CHARACTER SET %s COLLATE %s';
+
+	/**
+	 * CREATE TABLE keys flag
+	 *
+	 * Whether table keys are created from within the
+	 * CREATE TABLE statement.
+	 *
+	 * @var	bool
+	 */
+	protected $_create_table_keys	= TRUE;
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'TINYINT',
+		'SMALLINT',
+		'MEDIUMINT',
+		'INT',
+		'INTEGER',
+		'BIGINT',
+		'REAL',
+		'DOUBLE',
+		'DOUBLE PRECISION',
+		'FLOAT',
+		'DECIMAL',
+		'NUMERIC'
+	);
+
+	/**
+	 * NULL value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_null = 'NULL';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * CREATE TABLE attributes
+	 *
+	 * @param	array	$attributes	Associative array of table attributes
+	 * @return	string
+	 */
+	protected function _create_table_attr($attributes)
+	{
+		$sql = '';
+
+		foreach (array_keys($attributes) as $key)
+		{
+			if (is_string($key))
+			{
+				$sql .= ' '.strtoupper($key).' = '.$attributes[$key];
+			}
+		}
+
+		if ( ! empty($this->db->char_set) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET'))
+		{
+			$sql .= ' DEFAULT CHARACTER SET = '.$this->db->char_set;
+		}
+
+		if ( ! empty($this->db->dbcollat) && ! strpos($sql, 'COLLATE'))
+		{
+			$sql .= ' COLLATE = '.$this->db->dbcollat;
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if ($alter_type === 'DROP')
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table);
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			if ($field[$i]['_literal'] !== FALSE)
+			{
+				$field[$i] = ($alter_type === 'ADD')
+						? "\n\tADD ".$field[$i]['_literal']
+						: "\n\tMODIFY ".$field[$i]['_literal'];
+			}
+			else
+			{
+				if ($alter_type === 'ADD')
+				{
+					$field[$i]['_literal'] = "\n\tADD ";
+				}
+				else
+				{
+					$field[$i]['_literal'] = empty($field[$i]['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE ";
+				}
+
+				$field[$i] = $field[$i]['_literal'].$this->_process_column($field[$i]);
+			}
+		}
+
+		return array($sql.implode(',', $field));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process column
+	 *
+	 * @param	array	$field
+	 * @return	string
+	 */
+	protected function _process_column($field)
+	{
+		$extra_clause = isset($field['after'])
+			? ' AFTER '.$this->db->escape_identifiers($field['after']) : '';
+
+		if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE)
+		{
+			$extra_clause = ' FIRST';
+		}
+
+		return $this->db->escape_identifiers($field['name'])
+			.(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name']))
+			.' '.$field['type'].$field['length']
+			.$field['unsigned']
+			.$field['null']
+			.$field['default']
+			.$field['auto_increment']
+			.$field['unique']
+			.(empty($field['comment']) ? '' : ' COMMENT '.$field['comment'])
+			.$extra_clause;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process indexes
+	 *
+	 * @param	string	$table	(ignored)
+	 * @return	string
+	 */
+	protected function _process_indexes($table)
+	{
+		$sql = '';
+
+		for ($i = 0, $c = count($this->keys); $i < $c; $i++)
+		{
+			if (is_array($this->keys[$i]))
+			{
+				for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++)
+				{
+					if ( ! isset($this->fields[$this->keys[$i][$i2]]))
+					{
+						unset($this->keys[$i][$i2]);
+						continue;
+					}
+				}
+			}
+			elseif ( ! isset($this->fields[$this->keys[$i]]))
+			{
+				unset($this->keys[$i]);
+				continue;
+			}
+
+			is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]);
+
+			$sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i]))
+				.' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')';
+		}
+
+		$this->keys = array();
+
+		return $sql;
+	}
+
+}
diff --git a/system/database/drivers/mysql/mysql_result.php b/system/database/drivers/mysql/mysql_result.php
new file mode 100644
index 0000000..05fc36e
--- /dev/null
+++ b/system/database/drivers/mysql/mysql_result.php
@@ -0,0 +1,200 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * MySQL Result Class
+ *
+ * This class extends the parent result class: CI_DB_result
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_mysql_result extends CI_DB_result {
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	object	&$driver_object
+	 * @return	void
+	 */
+	public function __construct(&$driver_object)
+	{
+		parent::__construct($driver_object);
+
+		// Required, due to mysql_data_seek() causing nightmares
+		// with empty result sets
+		$this->num_rows = mysql_num_rows($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Number of rows in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_rows()
+	{
+		return $this->num_rows;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Number of fields in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_fields()
+	{
+		return mysql_num_fields($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * Generates an array of column names
+	 *
+	 * @return	array
+	 */
+	public function list_fields()
+	{
+		$field_names = array();
+		mysql_field_seek($this->result_id, 0);
+		while ($field = mysql_fetch_field($this->result_id))
+		{
+			$field_names[] = $field->name;
+		}
+
+		return $field_names;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data
+	 *
+	 * Generates an array of objects containing field meta-data
+	 *
+	 * @return	array
+	 */
+	public function field_data()
+	{
+		$retval = array();
+		for ($i = 0, $c = $this->num_fields(); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= mysql_field_name($this->result_id, $i);
+			$retval[$i]->type		= mysql_field_type($this->result_id, $i);
+			$retval[$i]->max_length		= mysql_field_len($this->result_id, $i);
+			$retval[$i]->primary_key	= (int) (strpos(mysql_field_flags($this->result_id, $i), 'primary_key') !== FALSE);
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Free the result
+	 *
+	 * @return	void
+	 */
+	public function free_result()
+	{
+		if (is_resource($this->result_id))
+		{
+			mysql_free_result($this->result_id);
+			$this->result_id = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Data Seek
+	 *
+	 * Moves the internal pointer to the desired offset. We call
+	 * this internally before fetching results to make sure the
+	 * result set starts at zero.
+	 *
+	 * @param	int	$n
+	 * @return	bool
+	 */
+	public function data_seek($n = 0)
+	{
+		return $this->num_rows
+			? mysql_data_seek($this->result_id, $n)
+			: FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - associative array
+	 *
+	 * Returns the result set as an array
+	 *
+	 * @return	array
+	 */
+	protected function _fetch_assoc()
+	{
+		return mysql_fetch_assoc($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - object
+	 *
+	 * Returns the result set as an object
+	 *
+	 * @param	string	$class_name
+	 * @return	object
+	 */
+	protected function _fetch_object($class_name = 'stdClass')
+	{
+		return mysql_fetch_object($this->result_id, $class_name);
+	}
+
+}
diff --git a/system/database/drivers/mysql/mysql_utility.php b/system/database/drivers/mysql/mysql_utility.php
new file mode 100644
index 0000000..0564a5a
--- /dev/null
+++ b/system/database/drivers/mysql/mysql_utility.php
@@ -0,0 +1,212 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * MySQL Utility Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_mysql_utility extends CI_DB_utility {
+
+	/**
+	 * List databases statement
+	 *
+	 * @var	string
+	 */
+	protected $_list_databases	= 'SHOW DATABASES';
+
+	/**
+	 * OPTIMIZE TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_optimize_table	= 'OPTIMIZE TABLE %s';
+
+	/**
+	 * REPAIR TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_repair_table	= 'REPAIR TABLE %s';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Export
+	 *
+	 * @param	array	$params	Preferences
+	 * @return	mixed
+	 */
+	protected function _backup($params = array())
+	{
+		if (count($params) === 0)
+		{
+			return FALSE;
+		}
+
+		// Extract the prefs for simplicity
+		extract($params);
+
+		// Build the output
+		$output = '';
+
+		// Do we need to include a statement to disable foreign key checks?
+		if ($foreign_key_checks === FALSE)
+		{
+			$output .= 'SET foreign_key_checks = 0;'.$newline;
+		}
+
+		foreach ( (array) $tables as $table)
+		{
+			// Is the table in the "ignore" list?
+			if (in_array($table, (array) $ignore, TRUE))
+			{
+				continue;
+			}
+
+			// Get the table schema
+			$query = $this->db->query('SHOW CREATE TABLE '.$this->db->escape_identifiers($this->db->database.'.'.$table));
+
+			// No result means the table name was invalid
+			if ($query === FALSE)
+			{
+				continue;
+			}
+
+			// Write out the table schema
+			$output .= '#'.$newline.'# TABLE STRUCTURE FOR: '.$table.$newline.'#'.$newline.$newline;
+
+			if ($add_drop === TRUE)
+			{
+				$output .= 'DROP TABLE IF EXISTS '.$this->db->protect_identifiers($table).';'.$newline.$newline;
+			}
+
+			$i = 0;
+			$result = $query->result_array();
+			foreach ($result[0] as $val)
+			{
+				if ($i++ % 2)
+				{
+					$output .= $val.';'.$newline.$newline;
+				}
+			}
+
+			// If inserts are not needed we're done...
+			if ($add_insert === FALSE)
+			{
+				continue;
+			}
+
+			// Grab all the data from the current table
+			$query = $this->db->query('SELECT * FROM '.$this->db->protect_identifiers($table));
+
+			if ($query->num_rows() === 0)
+			{
+				continue;
+			}
+
+			// Fetch the field names and determine if the field is an
+			// integer type. We use this info to decide whether to
+			// surround the data with quotes or not
+
+			$i = 0;
+			$field_str = '';
+			$is_int = array();
+			while ($field = mysql_fetch_field($query->result_id))
+			{
+				// Most versions of MySQL store timestamp as a string
+				$is_int[$i] = in_array(strtolower(mysql_field_type($query->result_id, $i)),
+							array('tinyint', 'smallint', 'mediumint', 'int', 'bigint'), //, 'timestamp'),
+							TRUE);
+
+				// Create a string of field names
+				$field_str .= $this->db->escape_identifiers($field->name).', ';
+				$i++;
+			}
+
+			// Trim off the end comma
+			$field_str = preg_replace('/, $/' , '', $field_str);
+
+			// Build the insert string
+			foreach ($query->result_array() as $row)
+			{
+				$val_str = '';
+
+				$i = 0;
+				foreach ($row as $v)
+				{
+					// Is the value NULL?
+					if ($v === NULL)
+					{
+						$val_str .= 'NULL';
+					}
+					else
+					{
+						// Escape the data if it's not an integer
+						$val_str .= ($is_int[$i] === FALSE) ? $this->db->escape($v) : $v;
+					}
+
+					// Append a comma
+					$val_str .= ', ';
+					$i++;
+				}
+
+				// Remove the comma at the end of the string
+				$val_str = preg_replace('/, $/' , '', $val_str);
+
+				// Build the INSERT string
+				$output .= 'INSERT INTO '.$this->db->protect_identifiers($table).' ('.$field_str.') VALUES ('.$val_str.');'.$newline;
+			}
+
+			$output .= $newline.$newline;
+		}
+
+		// Do we need to include a statement to re-enable foreign key checks?
+		if ($foreign_key_checks === FALSE)
+		{
+			$output .= 'SET foreign_key_checks = 1;'.$newline;
+		}
+
+		return $output;
+	}
+
+}
diff --git a/system/database/drivers/mysqli/index.html b/system/database/drivers/mysqli/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/drivers/mysqli/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/system/database/drivers/mysqli/mysqli_driver.php b/system/database/drivers/mysqli/mysqli_driver.php
new file mode 100644
index 0000000..f5e9949
--- /dev/null
+++ b/system/database/drivers/mysqli/mysqli_driver.php
@@ -0,0 +1,554 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * MySQLi Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_mysqli_driver extends CI_DB {
+
+	/**
+	 * Database driver
+	 *
+	 * @var	string
+	 */
+	public $dbdriver = 'mysqli';
+
+	/**
+	 * Compression flag
+	 *
+	 * @var	bool
+	 */
+	public $compress = FALSE;
+
+	/**
+	 * DELETE hack flag
+	 *
+	 * Whether to use the MySQL "delete hack" which allows the number
+	 * of affected rows to be shown. Uses a preg_replace when enabled,
+	 * adding a bit more processing to all queries.
+	 *
+	 * @var	bool
+	 */
+	public $delete_hack = TRUE;
+
+	/**
+	 * Strict ON flag
+	 *
+	 * Whether we're running in strict SQL mode.
+	 *
+	 * @var	bool
+	 */
+	public $stricton;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Identifier escape character
+	 *
+	 * @var	string
+	 */
+	protected $_escape_char = '`';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * MySQLi object
+	 *
+	 * Has to be preserved without being assigned to $conn_id.
+	 *
+	 * @var	MySQLi
+	 */
+	protected $_mysqli;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	object
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		// PHP 8.1 changes default error handling mode from silent to exceptions - reverse that
+		if (is_php('8.1'))
+		{
+			$mysqli_driver = new mysqli_driver();
+			$mysqli_driver->report_mode = MYSQLI_REPORT_OFF;
+		}
+
+		// Do we have a socket path?
+		if ($this->hostname[0] === '/')
+		{
+			$hostname = NULL;
+			$port = NULL;
+			$socket = $this->hostname;
+		}
+		else
+		{
+			$hostname = ($persistent === TRUE)
+				? 'p:'.$this->hostname : $this->hostname;
+			$port = empty($this->port) ? NULL : $this->port;
+			$socket = NULL;
+		}
+
+		$client_flags = ($this->compress === TRUE) ? MYSQLI_CLIENT_COMPRESS : 0;
+		$this->_mysqli = mysqli_init();
+
+		$this->_mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10);
+
+		if (isset($this->stricton))
+		{
+			if ($this->stricton)
+			{
+				$this->_mysqli->options(MYSQLI_INIT_COMMAND, 'SET SESSION sql_mode = CONCAT(@@sql_mode, ",", "STRICT_ALL_TABLES")');
+			}
+			else
+			{
+				$this->_mysqli->options(MYSQLI_INIT_COMMAND,
+					'SET SESSION sql_mode =
+					REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
+					@@sql_mode,
+					"STRICT_ALL_TABLES,", ""),
+					",STRICT_ALL_TABLES", ""),
+					"STRICT_ALL_TABLES", ""),
+					"STRICT_TRANS_TABLES,", ""),
+					",STRICT_TRANS_TABLES", ""),
+					"STRICT_TRANS_TABLES", "")'
+				);
+			}
+		}
+
+		if (is_array($this->encrypt))
+		{
+			$ssl = array();
+			empty($this->encrypt['ssl_key'])    OR $ssl['key']    = $this->encrypt['ssl_key'];
+			empty($this->encrypt['ssl_cert'])   OR $ssl['cert']   = $this->encrypt['ssl_cert'];
+			empty($this->encrypt['ssl_ca'])     OR $ssl['ca']     = $this->encrypt['ssl_ca'];
+			empty($this->encrypt['ssl_capath']) OR $ssl['capath'] = $this->encrypt['ssl_capath'];
+			empty($this->encrypt['ssl_cipher']) OR $ssl['cipher'] = $this->encrypt['ssl_cipher'];
+
+			if (isset($this->encrypt['ssl_verify']))
+			{
+				$client_flags |= MYSQLI_CLIENT_SSL;
+
+				if ($this->encrypt['ssl_verify'])
+				{
+					defined('MYSQLI_OPT_SSL_VERIFY_SERVER_CERT') && $this->_mysqli->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, TRUE);
+				}
+				// Apparently (when it exists), setting MYSQLI_OPT_SSL_VERIFY_SERVER_CERT
+				// to FALSE didn't do anything, so PHP 5.6.16 introduced yet another
+				// constant ...
+				//
+				// https://secure.php.net/ChangeLog-5.php#5.6.16
+				// https://bugs.php.net/bug.php?id=68344
+				elseif (defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT'))
+				{
+					$client_flags |= MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
+				}
+			}
+
+			if ( ! empty($ssl))
+			{
+				$client_flags |= MYSQLI_CLIENT_SSL;
+				$this->_mysqli->ssl_set(
+					isset($ssl['key'])    ? $ssl['key']    : NULL,
+					isset($ssl['cert'])   ? $ssl['cert']   : NULL,
+					isset($ssl['ca'])     ? $ssl['ca']     : NULL,
+					isset($ssl['capath']) ? $ssl['capath'] : NULL,
+					isset($ssl['cipher']) ? $ssl['cipher'] : NULL
+				);
+			}
+		}
+
+		if ($this->_mysqli->real_connect($hostname, $this->username, $this->password, $this->database, $port, $socket, $client_flags))
+		{
+			// Prior to version 5.7.3, MySQL silently downgrades to an unencrypted connection if SSL setup fails
+			if (
+				($client_flags & MYSQLI_CLIENT_SSL)
+				&& version_compare($this->_mysqli->client_info, '5.7.3', '<=')
+				&& empty($this->_mysqli->query("SHOW STATUS LIKE 'ssl_cipher'")->fetch_object()->Value)
+			)
+			{
+				$this->_mysqli->close();
+				$message = 'MySQLi was configured for an SSL connection, but got an unencrypted connection instead!';
+				log_message('error', $message);
+				return ($this->db_debug) ? $this->display_error($message, '', TRUE) : FALSE;
+			}
+
+			return $this->_mysqli;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Reconnect
+	 *
+	 * Keep / reestablish the db connection if no queries have been
+	 * sent for a length of time exceeding the server's idle timeout
+	 *
+	 * @return	void
+	 */
+	public function reconnect()
+	{
+		if ($this->conn_id !== FALSE && $this->conn_id->ping() === FALSE)
+		{
+			$this->conn_id = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Select the database
+	 *
+	 * @param	string	$database
+	 * @return	bool
+	 */
+	public function db_select($database = '')
+	{
+		if ($database === '')
+		{
+			$database = $this->database;
+		}
+
+		if ($this->conn_id->select_db($database))
+		{
+			$this->database = $database;
+			$this->data_cache = array();
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set client character set
+	 *
+	 * @param	string	$charset
+	 * @return	bool
+	 */
+	protected function _db_set_charset($charset)
+	{
+		return $this->conn_id->set_charset($charset);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database version number
+	 *
+	 * @return	string
+	 */
+	public function version()
+	{
+		if (isset($this->data_cache['version']))
+		{
+			return $this->data_cache['version'];
+		}
+
+		return $this->data_cache['version'] = $this->conn_id->server_info;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Execute the query
+	 *
+	 * @param	string	$sql	an SQL query
+	 * @return	mixed
+	 */
+	protected function _execute($sql)
+	{
+		return $this->conn_id->query($this->_prep_query($sql));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Prep the query
+	 *
+	 * If needed, each database adapter can prep the query string
+	 *
+	 * @param	string	$sql	an SQL query
+	 * @return	string
+	 */
+	protected function _prep_query($sql)
+	{
+		// mysqli_affected_rows() returns 0 for "DELETE FROM TABLE" queries. This hack
+		// modifies the query so that it a proper number of affected rows is returned.
+		if ($this->delete_hack === TRUE && preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql))
+		{
+			return trim($sql).' WHERE 1=1';
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Begin Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_begin()
+	{
+		$this->conn_id->autocommit(FALSE);
+		return is_php('5.5')
+			? $this->conn_id->begin_transaction()
+			: $this->simple_query('START TRANSACTION'); // can also be BEGIN or BEGIN WORK
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Commit Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_commit()
+	{
+		if ($this->conn_id->commit())
+		{
+			$this->conn_id->autocommit(TRUE);
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rollback Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_rollback()
+	{
+		if ($this->conn_id->rollback())
+		{
+			$this->conn_id->autocommit(TRUE);
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Platform-dependent string escape
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _escape_str($str)
+	{
+		return $this->conn_id->real_escape_string($str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Affected Rows
+	 *
+	 * @return	int
+	 */
+	public function affected_rows()
+	{
+		return $this->conn_id->affected_rows;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert ID
+	 *
+	 * @return	int
+	 */
+	public function insert_id()
+	{
+		return $this->conn_id->insert_id;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * List table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SHOW TABLES FROM '.$this->_escape_char.$this->database.$this->_escape_char;
+
+		if ($prefix_limit !== FALSE && $this->dbprefix !== '')
+		{
+			return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'";
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE)
+		{
+			return FALSE;
+		}
+		$query = $query->result_object();
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]->Field;
+
+			sscanf($query[$i]->Type, '%[a-z](%d)',
+				$retval[$i]->type,
+				$retval[$i]->max_length
+			);
+
+			$retval[$i]->default		= $query[$i]->Default;
+			$retval[$i]->primary_key	= (int) ($query[$i]->Key === 'PRI');
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error
+	 *
+	 * Returns an array containing code and message of the last
+	 * database error that has occurred.
+	 *
+	 * @return	array
+	 */
+	public function error()
+	{
+		if ( ! empty($this->_mysqli->connect_errno))
+		{
+			return array(
+				'code'    => $this->_mysqli->connect_errno,
+				'message' => $this->_mysqli->connect_error
+			);
+		}
+
+		return array('code' => $this->conn_id->errno, 'message' => $this->conn_id->error);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * FROM tables
+	 *
+	 * Groups tables in FROM clauses if needed, so there is no confusion
+	 * about operator precedence.
+	 *
+	 * @return	string
+	 */
+	protected function _from_tables()
+	{
+		if ( ! empty($this->qb_join) && count($this->qb_from) > 1)
+		{
+			return '('.implode(', ', $this->qb_from).')';
+		}
+
+		return implode(', ', $this->qb_from);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close DB Connection
+	 *
+	 * @return	void
+	 */
+	protected function _close()
+	{
+		$this->conn_id->close();
+	}
+
+}
diff --git a/system/database/drivers/mysqli/mysqli_forge.php b/system/database/drivers/mysqli/mysqli_forge.php
new file mode 100644
index 0000000..992c772
--- /dev/null
+++ b/system/database/drivers/mysqli/mysqli_forge.php
@@ -0,0 +1,245 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * MySQLi Forge Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_mysqli_forge extends CI_DB_forge {
+
+	/**
+	 * CREATE DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_database	= 'CREATE DATABASE %s CHARACTER SET %s COLLATE %s';
+
+	/**
+	 * CREATE TABLE keys flag
+	 *
+	 * Whether table keys are created from within the
+	 * CREATE TABLE statement.
+	 *
+	 * @var	bool
+	 */
+	protected $_create_table_keys	= TRUE;
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'TINYINT',
+		'SMALLINT',
+		'MEDIUMINT',
+		'INT',
+		'INTEGER',
+		'BIGINT',
+		'REAL',
+		'DOUBLE',
+		'DOUBLE PRECISION',
+		'FLOAT',
+		'DECIMAL',
+		'NUMERIC'
+	);
+
+	/**
+	 * NULL value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_null = 'NULL';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * CREATE TABLE attributes
+	 *
+	 * @param	array	$attributes	Associative array of table attributes
+	 * @return	string
+	 */
+	protected function _create_table_attr($attributes)
+	{
+		$sql = '';
+
+		foreach (array_keys($attributes) as $key)
+		{
+			if (is_string($key))
+			{
+				$sql .= ' '.strtoupper($key).' = '.$attributes[$key];
+			}
+		}
+
+		if ( ! empty($this->db->char_set) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET'))
+		{
+			$sql .= ' DEFAULT CHARACTER SET = '.$this->db->char_set;
+		}
+
+		if ( ! empty($this->db->dbcollat) && ! strpos($sql, 'COLLATE'))
+		{
+			$sql .= ' COLLATE = '.$this->db->dbcollat;
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if ($alter_type === 'DROP')
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table);
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			if ($field[$i]['_literal'] !== FALSE)
+			{
+				$field[$i] = ($alter_type === 'ADD')
+						? "\n\tADD ".$field[$i]['_literal']
+						: "\n\tMODIFY ".$field[$i]['_literal'];
+			}
+			else
+			{
+				if ($alter_type === 'ADD')
+				{
+					$field[$i]['_literal'] = "\n\tADD ";
+				}
+				else
+				{
+					$field[$i]['_literal'] = empty($field[$i]['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE ";
+				}
+
+				$field[$i] = $field[$i]['_literal'].$this->_process_column($field[$i]);
+			}
+		}
+
+		return array($sql.implode(',', $field));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process column
+	 *
+	 * @param	array	$field
+	 * @return	string
+	 */
+	protected function _process_column($field)
+	{
+		$extra_clause = isset($field['after'])
+			? ' AFTER '.$this->db->escape_identifiers($field['after']) : '';
+
+		if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE)
+		{
+			$extra_clause = ' FIRST';
+		}
+
+		return $this->db->escape_identifiers($field['name'])
+			.(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name']))
+			.' '.$field['type'].$field['length']
+			.$field['unsigned']
+			.$field['null']
+			.$field['default']
+			.$field['auto_increment']
+			.$field['unique']
+			.(empty($field['comment']) ? '' : ' COMMENT '.$field['comment'])
+			.$extra_clause;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process indexes
+	 *
+	 * @param	string	$table	(ignored)
+	 * @return	string
+	 */
+	protected function _process_indexes($table)
+	{
+		$sql = '';
+
+		for ($i = 0, $c = count($this->keys); $i < $c; $i++)
+		{
+			if (is_array($this->keys[$i]))
+			{
+				for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++)
+				{
+					if ( ! isset($this->fields[$this->keys[$i][$i2]]))
+					{
+						unset($this->keys[$i][$i2]);
+						continue;
+					}
+				}
+			}
+			elseif ( ! isset($this->fields[$this->keys[$i]]))
+			{
+				unset($this->keys[$i]);
+				continue;
+			}
+
+			is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]);
+
+			$sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i]))
+				.' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')';
+		}
+
+		$this->keys = array();
+
+		return $sql;
+	}
+
+}
diff --git a/system/database/drivers/mysqli/mysqli_result.php b/system/database/drivers/mysqli/mysqli_result.php
new file mode 100644
index 0000000..8c4f94d
--- /dev/null
+++ b/system/database/drivers/mysqli/mysqli_result.php
@@ -0,0 +1,233 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * MySQLi Result Class
+ *
+ * This class extends the parent result class: CI_DB_result
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_mysqli_result extends CI_DB_result {
+
+	/**
+	 * Number of rows in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_rows()
+	{
+		return is_int($this->num_rows)
+			? $this->num_rows
+			: $this->num_rows = $this->result_id->num_rows;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Number of fields in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_fields()
+	{
+		return $this->result_id->field_count;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * Generates an array of column names
+	 *
+	 * @return	array
+	 */
+	public function list_fields()
+	{
+		$field_names = array();
+		$this->result_id->field_seek(0);
+		while ($field = $this->result_id->fetch_field())
+		{
+			$field_names[] = $field->name;
+		}
+
+		return $field_names;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data
+	 *
+	 * Generates an array of objects containing field meta-data
+	 *
+	 * @return	array
+	 */
+	public function field_data()
+	{
+		$retval = array();
+		$field_data = $this->result_id->fetch_fields();
+		for ($i = 0, $c = count($field_data); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $field_data[$i]->name;
+			$retval[$i]->type		= static::_get_field_type($field_data[$i]->type);
+			$retval[$i]->max_length		= $field_data[$i]->max_length;
+			$retval[$i]->primary_key	= (int) ($field_data[$i]->flags & MYSQLI_PRI_KEY_FLAG);
+			$retval[$i]->default		= $field_data[$i]->def;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get field type
+	 *
+	 * Extracts field type info from the bitflags returned by
+	 * mysqli_result::fetch_fields()
+	 *
+	 * @used-by	CI_DB_mysqli_result::field_data()
+	 * @param	int	$type
+	 * @return	string
+	 */
+	private static function _get_field_type($type)
+	{
+		static $map;
+		isset($map) OR $map = array(
+			MYSQLI_TYPE_DECIMAL     => 'decimal',
+			MYSQLI_TYPE_BIT         => 'bit',
+			MYSQLI_TYPE_TINY        => 'tinyint',
+			MYSQLI_TYPE_SHORT       => 'smallint',
+			MYSQLI_TYPE_INT24       => 'mediumint',
+			MYSQLI_TYPE_LONG        => 'int',
+			MYSQLI_TYPE_LONGLONG    => 'bigint',
+			MYSQLI_TYPE_FLOAT       => 'float',
+			MYSQLI_TYPE_DOUBLE      => 'double',
+			MYSQLI_TYPE_TIMESTAMP   => 'timestamp',
+			MYSQLI_TYPE_DATE        => 'date',
+			MYSQLI_TYPE_TIME        => 'time',
+			MYSQLI_TYPE_DATETIME    => 'datetime',
+			MYSQLI_TYPE_YEAR        => 'year',
+			MYSQLI_TYPE_NEWDATE     => 'date',
+			MYSQLI_TYPE_INTERVAL    => 'interval',
+			MYSQLI_TYPE_ENUM        => 'enum',
+			MYSQLI_TYPE_SET         => 'set',
+			MYSQLI_TYPE_TINY_BLOB   => 'tinyblob',
+			MYSQLI_TYPE_MEDIUM_BLOB => 'mediumblob',
+			MYSQLI_TYPE_BLOB        => 'blob',
+			MYSQLI_TYPE_LONG_BLOB   => 'longblob',
+			MYSQLI_TYPE_STRING      => 'char',
+			MYSQLI_TYPE_VAR_STRING  => 'varchar',
+			MYSQLI_TYPE_GEOMETRY    => 'geometry'
+		);
+
+		return isset($map[$type]) ? $map[$type] : $type;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Free the result
+	 *
+	 * @return	void
+	 */
+	public function free_result()
+	{
+		if (is_object($this->result_id))
+		{
+			$this->result_id->free();
+			$this->result_id = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Data Seek
+	 *
+	 * Moves the internal pointer to the desired offset. We call
+	 * this internally before fetching results to make sure the
+	 * result set starts at zero.
+	 *
+	 * @param	int	$n
+	 * @return	bool
+	 */
+	public function data_seek($n = 0)
+	{
+		return $this->result_id->data_seek($n);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - associative array
+	 *
+	 * Returns the result set as an array
+	 *
+	 * @return	array
+	 */
+	protected function _fetch_assoc()
+	{
+		return $this->result_id->fetch_assoc();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - object
+	 *
+	 * Returns the result set as an object
+	 *
+	 * @param	string	$class_name
+	 * @return	object
+	 */
+	protected function _fetch_object($class_name = 'stdClass')
+	{
+		return $this->result_id->fetch_object($class_name);
+	}
+
+}
diff --git a/system/database/drivers/mysqli/mysqli_utility.php b/system/database/drivers/mysqli/mysqli_utility.php
new file mode 100644
index 0000000..6a7d419
--- /dev/null
+++ b/system/database/drivers/mysqli/mysqli_utility.php
@@ -0,0 +1,212 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * MySQLi Utility Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_mysqli_utility extends CI_DB_utility {
+
+	/**
+	 * List databases statement
+	 *
+	 * @var	string
+	 */
+	protected $_list_databases	= 'SHOW DATABASES';
+
+	/**
+	 * OPTIMIZE TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_optimize_table	= 'OPTIMIZE TABLE %s';
+
+	/**
+	 * REPAIR TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_repair_table	= 'REPAIR TABLE %s';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Export
+	 *
+	 * @param	array	$params	Preferences
+	 * @return	mixed
+	 */
+	protected function _backup($params = array())
+	{
+		if (count($params) === 0)
+		{
+			return FALSE;
+		}
+
+		// Extract the prefs for simplicity
+		extract($params);
+
+		// Build the output
+		$output = '';
+
+		// Do we need to include a statement to disable foreign key checks?
+		if ($foreign_key_checks === FALSE)
+		{
+			$output .= 'SET foreign_key_checks = 0;'.$newline;
+		}
+
+		foreach ( (array) $tables as $table)
+		{
+			// Is the table in the "ignore" list?
+			if (in_array($table, (array) $ignore, TRUE))
+			{
+				continue;
+			}
+
+			// Get the table schema
+			$query = $this->db->query('SHOW CREATE TABLE '.$this->db->escape_identifiers($this->db->database.'.'.$table));
+
+			// No result means the table name was invalid
+			if ($query === FALSE)
+			{
+				continue;
+			}
+
+			// Write out the table schema
+			$output .= '#'.$newline.'# TABLE STRUCTURE FOR: '.$table.$newline.'#'.$newline.$newline;
+
+			if ($add_drop === TRUE)
+			{
+				$output .= 'DROP TABLE IF EXISTS '.$this->db->protect_identifiers($table).';'.$newline.$newline;
+			}
+
+			$i = 0;
+			$result = $query->result_array();
+			foreach ($result[0] as $val)
+			{
+				if ($i++ % 2)
+				{
+					$output .= $val.';'.$newline.$newline;
+				}
+			}
+
+			// If inserts are not needed we're done...
+			if ($add_insert === FALSE)
+			{
+				continue;
+			}
+
+			// Grab all the data from the current table
+			$query = $this->db->query('SELECT * FROM '.$this->db->protect_identifiers($table));
+
+			if ($query->num_rows() === 0)
+			{
+				continue;
+			}
+
+			// Fetch the field names and determine if the field is an
+			// integer type. We use this info to decide whether to
+			// surround the data with quotes or not
+
+			$i = 0;
+			$field_str = '';
+			$is_int = array();
+			while ($field = $query->result_id->fetch_field())
+			{
+				// Most versions of MySQL store timestamp as a string
+				$is_int[$i] = in_array($field->type, array(MYSQLI_TYPE_TINY, MYSQLI_TYPE_SHORT, MYSQLI_TYPE_INT24, MYSQLI_TYPE_LONG), TRUE);
+
+				// Create a string of field names
+				$field_str .= $this->db->escape_identifiers($field->name).', ';
+				$i++;
+			}
+
+			// Trim off the end comma
+			$field_str = preg_replace('/, $/' , '', $field_str);
+
+			// Build the insert string
+			foreach ($query->result_array() as $row)
+			{
+				$val_str = '';
+
+				$i = 0;
+				foreach ($row as $v)
+				{
+					// Is the value NULL?
+					if ($v === NULL)
+					{
+						$val_str .= 'NULL';
+					}
+					else
+					{
+						// Escape the data if it's not an integer
+						$val_str .= ($is_int[$i] === FALSE) ? $this->db->escape($v) : $v;
+					}
+
+					// Append a comma
+					$val_str .= ', ';
+					$i++;
+				}
+
+				// Remove the comma at the end of the string
+				$val_str = preg_replace('/, $/' , '', $val_str);
+
+				// Build the INSERT string
+				$output .= 'INSERT INTO '.$this->db->protect_identifiers($table).' ('.$field_str.') VALUES ('.$val_str.');'.$newline;
+			}
+
+			$output .= $newline.$newline;
+		}
+
+		// Do we need to include a statement to re-enable foreign key checks?
+		if ($foreign_key_checks === FALSE)
+		{
+			$output .= 'SET foreign_key_checks = 1;'.$newline;
+		}
+
+		return $output;
+	}
+
+}
diff --git a/system/database/drivers/oci8/index.html b/system/database/drivers/oci8/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/drivers/oci8/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/system/database/drivers/oci8/oci8_driver.php b/system/database/drivers/oci8/oci8_driver.php
new file mode 100644
index 0000000..7bb43b5
--- /dev/null
+++ b/system/database/drivers/oci8/oci8_driver.php
@@ -0,0 +1,712 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.4.1
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * oci8 Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage  Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+
+/**
+ * oci8 Database Adapter Class
+ *
+ * This is a modification of the DB_driver class to
+ * permit access to oracle databases
+ *
+ * @author	  Kelly McArdle
+ */
+class CI_DB_oci8_driver extends CI_DB {
+
+	/**
+	 * Database driver
+	 *
+	 * @var	string
+	 */
+	public $dbdriver = 'oci8';
+
+	/**
+	 * Statement ID
+	 *
+	 * @var	resource
+	 */
+	public $stmt_id;
+
+	/**
+	 * Cursor ID
+	 *
+	 * @var	resource
+	 */
+	public $curs_id;
+
+	/**
+	 * Commit mode flag
+	 *
+	 * @var	int
+	 */
+	public $commit_mode = OCI_COMMIT_ON_SUCCESS;
+
+	/**
+	 * Limit used flag
+	 *
+	 * If we use LIMIT, we'll add a field that will
+	 * throw off num_fields later.
+	 *
+	 * @var	bool
+	 */
+	public $limit_used = FALSE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Reset $stmt_id flag
+	 *
+	 * Used by stored_procedure() to prevent _execute() from
+	 * re-setting the statement ID.
+	 */
+	protected $_reset_stmt_id = TRUE;
+
+	/**
+	 * List of reserved identifiers
+	 *
+	 * Identifiers that must NOT be escaped.
+	 *
+	 * @var	string[]
+	 */
+	protected $_reserved_identifiers = array('*', 'rownum');
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('ASC', 'ASC'); // not currently supported
+
+	/**
+	 * COUNT string
+	 *
+	 * @used-by	CI_DB_driver::count_all()
+	 * @used-by	CI_DB_query_builder::count_all_results()
+	 *
+	 * @var	string
+	 */
+	protected $_count_string = 'SELECT COUNT(1) AS ';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		$valid_dsns = array(
+			'tns'	=> '/^\(DESCRIPTION=(\(.+\)){2,}\)$/', // TNS
+			// Easy Connect string (Oracle 10g+)
+			'ec'	=> '/^(\/\/)?[a-z0-9.:_-]+(:[1-9][0-9]{0,4})?(\/[a-z0-9$_]+)?(:[^\/])?(\/[a-z0-9$_]+)?$/i',
+			'in'	=> '/^[a-z0-9$_]+$/i' // Instance name (defined in tnsnames.ora)
+		);
+
+		/* Space characters don't have any effect when actually
+		 * connecting, but can be a hassle while validating the DSN.
+		 */
+		$this->dsn = str_replace(array("\n", "\r", "\t", ' '), '', $this->dsn);
+
+		if ($this->dsn !== '')
+		{
+			foreach ($valid_dsns as $regexp)
+			{
+				if (preg_match($regexp, $this->dsn))
+				{
+					return;
+				}
+			}
+		}
+
+		// Legacy support for TNS in the hostname configuration field
+		$this->hostname = str_replace(array("\n", "\r", "\t", ' '), '', $this->hostname);
+		if (preg_match($valid_dsns['tns'], $this->hostname))
+		{
+			$this->dsn = $this->hostname;
+			return;
+		}
+		elseif ($this->hostname !== '' && strpos($this->hostname, '/') === FALSE && strpos($this->hostname, ':') === FALSE
+			&& (( ! empty($this->port) && ctype_digit($this->port)) OR $this->database !== ''))
+		{
+			/* If the hostname field isn't empty, doesn't contain
+			 * ':' and/or '/' and if port and/or database aren't
+			 * empty, then the hostname field is most likely indeed
+			 * just a hostname. Therefore we'll try and build an
+			 * Easy Connect string from these 3 settings, assuming
+			 * that the database field is a service name.
+			 */
+			$this->dsn = $this->hostname
+				.(( ! empty($this->port) && ctype_digit($this->port)) ? ':'.$this->port : '')
+				.($this->database !== '' ? '/'.ltrim($this->database, '/') : '');
+
+			if (preg_match($valid_dsns['ec'], $this->dsn))
+			{
+				return;
+			}
+		}
+
+		/* At this point, we can only try and validate the hostname and
+		 * database fields separately as DSNs.
+		 */
+		if (preg_match($valid_dsns['ec'], $this->hostname) OR preg_match($valid_dsns['in'], $this->hostname))
+		{
+			$this->dsn = $this->hostname;
+			return;
+		}
+
+		$this->database = str_replace(array("\n", "\r", "\t", ' '), '', $this->database);
+		foreach ($valid_dsns as $regexp)
+		{
+			if (preg_match($regexp, $this->database))
+			{
+				return;
+			}
+		}
+
+		/* Well - OK, an empty string should work as well.
+		 * PHP will try to use environment variables to
+		 * determine which Oracle instance to connect to.
+		 */
+		$this->dsn = '';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Non-persistent database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	resource
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		$func = ($persistent === TRUE) ? 'oci_pconnect' : 'oci_connect';
+		return empty($this->char_set)
+			? $func($this->username, $this->password, $this->dsn)
+			: $func($this->username, $this->password, $this->dsn, $this->char_set);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database version number
+	 *
+	 * @return	string
+	 */
+	public function version()
+	{
+		if (isset($this->data_cache['version']))
+		{
+			return $this->data_cache['version'];
+		}
+
+		if ( ! $this->conn_id OR ($version_string = oci_server_version($this->conn_id)) === FALSE)
+		{
+			return FALSE;
+		}
+		elseif (preg_match('#Release\s(\d+(?:\.\d+)+)#', $version_string, $match))
+		{
+			return $this->data_cache['version'] = $match[1];
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Execute the query
+	 *
+	 * @param	string	$sql	an SQL query
+	 * @return	resource
+	 */
+	protected function _execute($sql)
+	{
+		/* Oracle must parse the query before it is run. All of the actions with
+		 * the query are based on the statement id returned by oci_parse().
+		 */
+		if ($this->_reset_stmt_id === TRUE)
+		{
+			$this->stmt_id = oci_parse($this->conn_id, $sql);
+		}
+
+		oci_set_prefetch($this->stmt_id, 1000);
+		return oci_execute($this->stmt_id, $this->commit_mode);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get cursor. Returns a cursor from the database
+	 *
+	 * @return	resource
+	 */
+	public function get_cursor()
+	{
+		return $this->curs_id = oci_new_cursor($this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Stored Procedure.  Executes a stored procedure
+	 *
+	 * @param	string	package name in which the stored procedure is in
+	 * @param	string	stored procedure name to execute
+	 * @param	array	parameters
+	 * @return	mixed
+	 *
+	 * params array keys
+	 *
+	 * KEY      OPTIONAL  NOTES
+	 * name     no        the name of the parameter should be in :<param_name> format
+	 * value    no        the value of the parameter.  If this is an OUT or IN OUT parameter,
+	 *                    this should be a reference to a variable
+	 * type     yes       the type of the parameter
+	 * length   yes       the max size of the parameter
+	 */
+	public function stored_procedure($package, $procedure, array $params)
+	{
+		if ($package === '' OR $procedure === '')
+		{
+			log_message('error', 'Invalid query: '.$package.'.'.$procedure);
+			return ($this->db_debug) ? $this->display_error('db_invalid_query') : FALSE;
+		}
+
+		// Build the query string
+		$sql = 'BEGIN '.$package.'.'.$procedure.'(';
+
+		$have_cursor = FALSE;
+		foreach ($params as $param)
+		{
+			$sql .= $param['name'].',';
+
+			if (isset($param['type']) && $param['type'] === OCI_B_CURSOR)
+			{
+				$have_cursor = TRUE;
+			}
+		}
+		$sql = trim($sql, ',').'); END;';
+
+		$this->_reset_stmt_id = FALSE;
+		$this->stmt_id = oci_parse($this->conn_id, $sql);
+		$this->_bind_params($params);
+		$result = $this->query($sql, FALSE, $have_cursor);
+		$this->_reset_stmt_id = TRUE;
+		return $result;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Bind parameters
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	protected function _bind_params($params)
+	{
+		if ( ! is_array($params) OR ! is_resource($this->stmt_id))
+		{
+			return;
+		}
+
+		foreach ($params as $param)
+		{
+			foreach (array('name', 'value', 'type', 'length') as $val)
+			{
+				if ( ! isset($param[$val]))
+				{
+					$param[$val] = '';
+				}
+			}
+
+			oci_bind_by_name($this->stmt_id, $param['name'], $param['value'], $param['length'], $param['type']);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Begin Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_begin()
+	{
+		$this->commit_mode = OCI_NO_AUTO_COMMIT;
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Commit Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_commit()
+	{
+		$this->commit_mode = OCI_COMMIT_ON_SUCCESS;
+
+		return oci_commit($this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rollback Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_rollback()
+	{
+		$this->commit_mode = OCI_COMMIT_ON_SUCCESS;
+		return oci_rollback($this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Affected Rows
+	 *
+	 * @return	int
+	 */
+	public function affected_rows()
+	{
+		return oci_num_rows($this->stmt_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert ID
+	 *
+	 * @return	int
+	 */
+	public function insert_id()
+	{
+		// not supported in oracle
+		return $this->display_error('db_unsupported_function');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SELECT "TABLE_NAME" FROM "ALL_TABLES"';
+
+		if ($prefix_limit !== FALSE && $this->dbprefix !== '')
+		{
+			return $sql.' WHERE "TABLE_NAME" LIKE \''.$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		if (strpos($table, '.') !== FALSE)
+		{
+			sscanf($table, '%[^.].%s', $owner, $table);
+		}
+		else
+		{
+			$owner = $this->username;
+		}
+
+		return 'SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS
+			WHERE UPPER(OWNER) = '.$this->escape(strtoupper($owner)).'
+				AND UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		if (strpos($table, '.') !== FALSE)
+		{
+			sscanf($table, '%[^.].%s', $owner, $table);
+		}
+		else
+		{
+			$owner = $this->username;
+		}
+
+		$sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHAR_LENGTH, DATA_PRECISION, DATA_LENGTH, DATA_DEFAULT, NULLABLE
+			FROM ALL_TAB_COLUMNS
+			WHERE UPPER(OWNER) = '.$this->escape(strtoupper($owner)).'
+				AND UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
+
+		if (($query = $this->query($sql)) === FALSE)
+		{
+			return FALSE;
+		}
+		$query = $query->result_object();
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]->COLUMN_NAME;
+			$retval[$i]->type		= $query[$i]->DATA_TYPE;
+
+			$length = ($query[$i]->CHAR_LENGTH > 0)
+				? $query[$i]->CHAR_LENGTH : $query[$i]->DATA_PRECISION;
+			if ($length === NULL)
+			{
+				$length = $query[$i]->DATA_LENGTH;
+			}
+			$retval[$i]->max_length		= $length;
+
+			$default = $query[$i]->DATA_DEFAULT;
+			if ($default === NULL && $query[$i]->NULLABLE === 'N')
+			{
+				$default = '';
+			}
+			$retval[$i]->default = $default;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error
+	 *
+	 * Returns an array containing code and message of the last
+	 * database error that has occurred.
+	 *
+	 * @return	array
+	 */
+	public function error()
+	{
+		// oci_error() returns an array that already contains
+		// 'code' and 'message' keys, but it can return false
+		// if there was no error ....
+		if (is_resource($this->curs_id))
+		{
+			$error = oci_error($this->curs_id);
+		}
+		elseif (is_resource($this->stmt_id))
+		{
+			$error = oci_error($this->stmt_id);
+		}
+		elseif (is_resource($this->conn_id))
+		{
+			$error = oci_error($this->conn_id);
+		}
+		else
+		{
+			$error = oci_error();
+		}
+
+		return is_array($error)
+			? $error
+			: array('code' => '', 'message' => '');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert batch statement
+	 *
+	 * Generates a platform-specific insert string from the supplied data
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$keys	INSERT keys
+	 * @param 	array	$values	INSERT values
+	 * @return	string
+	 */
+	protected function _insert_batch($table, $keys, $values)
+	{
+		$keys = implode(', ', $keys);
+		$sql = "INSERT ALL\n";
+
+		for ($i = 0, $c = count($values); $i < $c; $i++)
+		{
+			$sql .= '	INTO '.$table.' ('.$keys.') VALUES '.$values[$i]."\n";
+		}
+
+		return $sql.'SELECT * FROM dual';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Truncate statement
+	 *
+	 * Generates a platform-specific truncate string from the supplied data
+	 *
+	 * If the database does not support the TRUNCATE statement,
+	 * then this method maps to 'DELETE FROM table'
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _truncate($table)
+	{
+		return 'TRUNCATE TABLE '.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete statement
+	 *
+	 * Generates a platform-specific delete string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _delete($table)
+	{
+		if ($this->qb_limit)
+		{
+			$this->where('rownum <= ',$this->qb_limit, FALSE);
+			$this->qb_limit = FALSE;
+		}
+
+		return parent::_delete($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT
+	 *
+	 * Generates a platform-specific LIMIT clause
+	 *
+	 * @param	string	$sql	SQL Query
+	 * @return	string
+	 */
+	protected function _limit($sql)
+	{
+		if (version_compare($this->version(), '12.1', '>='))
+		{
+			// OFFSET-FETCH can be used only with the ORDER BY clause
+			empty($this->qb_orderby) && $sql .= ' ORDER BY 1';
+
+			return $sql.' OFFSET '.(int) $this->qb_offset.' ROWS FETCH NEXT '.$this->qb_limit.' ROWS ONLY';
+		}
+
+		$this->limit_used = TRUE;
+		return 'SELECT * FROM (SELECT inner_query.*, rownum rnum FROM ('.$sql.') inner_query WHERE rownum < '.($this->qb_offset + $this->qb_limit + 1).')'
+			.($this->qb_offset ? ' WHERE rnum >= '.($this->qb_offset + 1) : '');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close DB Connection
+	 *
+	 * @return	void
+	 */
+	protected function _close()
+	{
+		if (is_resource($this->curs_id))
+		{
+			oci_free_statement($this->curs_id);
+		}
+
+		if (is_resource($this->stmt_id))
+		{
+			oci_free_statement($this->stmt_id);
+		}
+
+		oci_close($this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * We need to reset our $limit_used hack flag, so it doesn't propagate
+	 * to subsequent queries.
+	 *
+	 * @return	void
+	 */
+	protected function _reset_select()
+	{
+		$this->limit_used = FALSE;
+		parent::_reset_select();
+	}
+}
diff --git a/system/database/drivers/oci8/oci8_forge.php b/system/database/drivers/oci8/oci8_forge.php
new file mode 100644
index 0000000..9910b11
--- /dev/null
+++ b/system/database/drivers/oci8/oci8_forge.php
@@ -0,0 +1,217 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.4.1
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Oracle Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_oci8_forge extends CI_DB_forge {
+
+	/**
+	 * CREATE DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_database	= FALSE;
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= FALSE;
+
+	/**
+	 * DROP DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_database	= FALSE;
+
+	/**
+	 * DROP TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_table_if	= FALSE;
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	bool|array
+	 */
+	protected $_unsigned		= FALSE;
+
+	/**
+	 * NULL value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_null		= 'NULL';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if ($alter_type === 'DROP')
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+		elseif ($alter_type === 'CHANGE')
+		{
+			$alter_type = 'MODIFY';
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table);
+		$sqls = array();
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			if ($field[$i]['_literal'] !== FALSE)
+			{
+				$field[$i] = "\n\t".$field[$i]['_literal'];
+			}
+			else
+			{
+				$field[$i]['_literal'] = "\n\t".$this->_process_column($field[$i]);
+
+				if ( ! empty($field[$i]['comment']))
+				{
+					$sqls[] = 'COMMENT ON COLUMN '
+						.$this->db->escape_identifiers($table).'.'.$this->db->escape_identifiers($field[$i]['name'])
+						.' IS '.$field[$i]['comment'];
+				}
+
+				if ($alter_type === 'MODIFY' && ! empty($field[$i]['new_name']))
+				{
+					$sqls[] = $sql.' RENAME COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+						.' TO '.$this->db->escape_identifiers($field[$i]['new_name']);
+				}
+
+				$field[$i] = "\n\t".$field[$i]['_literal'];
+			}
+		}
+
+		$sql .= ' '.$alter_type.' ';
+		$sql .= (count($field) === 1)
+				? $field[0]
+				: '('.implode(',', $field).')';
+
+		// RENAME COLUMN must be executed after MODIFY
+		array_unshift($sqls, $sql);
+		return $sqls;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'number') !== FALSE && version_compare($this->db->version(), '12.1', '>='))
+		{
+			$field['auto_increment'] = ' GENERATED ALWAYS AS IDENTITY';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process column
+	 *
+	 * @param	array	$field
+	 * @return	string
+	 */
+	protected function _process_column($field)
+	{
+		return $this->db->escape_identifiers($field['name'])
+			.' '.$field['type'].$field['length']
+			.$field['unsigned']
+			.$field['default']
+			.$field['auto_increment']
+			.$field['null']
+			.$field['unique'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'TINYINT':
+				$attributes['TYPE'] = 'NUMBER';
+				return;
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'NUMBER';
+				return;
+			case 'INT':
+				$attributes['TYPE'] = 'NUMBER';
+				return;
+			case 'BIGINT':
+				$attributes['TYPE'] = 'NUMBER';
+				return;
+			default: return;
+		}
+	}
+}
diff --git a/system/database/drivers/oci8/oci8_result.php b/system/database/drivers/oci8/oci8_result.php
new file mode 100644
index 0000000..4312f9b
--- /dev/null
+++ b/system/database/drivers/oci8/oci8_result.php
@@ -0,0 +1,230 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.4.1
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * oci8 Result Class
+ *
+ * This class extends the parent result class: CI_DB_result
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_oci8_result extends CI_DB_result {
+
+	/**
+	 * Statement ID
+	 *
+	 * @var	resource
+	 */
+	public $stmt_id;
+
+	/**
+	 * Cursor ID
+	 *
+	 * @var	resource
+	 */
+	public $curs_id;
+
+	/**
+	 * Limit used flag
+	 *
+	 * @var	bool
+	 */
+	public $limit_used;
+
+	/**
+	 * Commit mode flag
+	 *
+	 * @var	int
+	 */
+	public $commit_mode;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	object	&$driver_object
+	 * @return	void
+	 */
+	public function __construct(&$driver_object)
+	{
+		parent::__construct($driver_object);
+
+		$this->stmt_id = $driver_object->stmt_id;
+		$this->curs_id = $driver_object->curs_id;
+		$this->limit_used = $driver_object->limit_used;
+		$this->commit_mode =& $driver_object->commit_mode;
+		$driver_object->stmt_id = FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Number of fields in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_fields()
+	{
+		$count = oci_num_fields($this->stmt_id);
+
+		// if we used a limit we subtract it
+		return ($this->limit_used) ? $count - 1 : $count;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * Generates an array of column names
+	 *
+	 * @return	array
+	 */
+	public function list_fields()
+	{
+		$field_names = array();
+		for ($c = 1, $fieldCount = $this->num_fields(); $c <= $fieldCount; $c++)
+		{
+			$field_names[] = oci_field_name($this->stmt_id, $c);
+		}
+		return $field_names;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data
+	 *
+	 * Generates an array of objects containing field meta-data
+	 *
+	 * @return	array
+	 */
+	public function field_data()
+	{
+		$retval = array();
+		for ($c = 1, $fieldCount = $this->num_fields(); $c <= $fieldCount; $c++)
+		{
+			$F		= new stdClass();
+			$F->name	= oci_field_name($this->stmt_id, $c);
+			$F->type	= oci_field_type($this->stmt_id, $c);
+			$F->max_length	= oci_field_size($this->stmt_id, $c);
+
+			$retval[] = $F;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Free the result
+	 *
+	 * @return	void
+	 */
+	public function free_result()
+	{
+		if (is_resource($this->result_id))
+		{
+			oci_free_statement($this->result_id);
+			$this->result_id = FALSE;
+		}
+
+		if (is_resource($this->stmt_id))
+		{
+			oci_free_statement($this->stmt_id);
+		}
+
+		if (is_resource($this->curs_id))
+		{
+			oci_cancel($this->curs_id);
+			$this->curs_id = NULL;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - associative array
+	 *
+	 * Returns the result set as an array
+	 *
+	 * @return	array
+	 */
+	protected function _fetch_assoc()
+	{
+		$id = ($this->curs_id) ? $this->curs_id : $this->stmt_id;
+		return oci_fetch_assoc($id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - object
+	 *
+	 * Returns the result set as an object
+	 *
+	 * @param	string	$class_name
+	 * @return	object
+	 */
+	protected function _fetch_object($class_name = 'stdClass')
+	{
+		$row = ($this->curs_id)
+			? oci_fetch_object($this->curs_id)
+			: oci_fetch_object($this->stmt_id);
+
+		if ($class_name === 'stdClass' OR ! $row)
+		{
+			return $row;
+		}
+
+		$class_name = new $class_name();
+		foreach ($row as $key => $value)
+		{
+			$class_name->$key = $value;
+		}
+
+		return $class_name;
+	}
+
+}
diff --git a/system/database/drivers/oci8/oci8_utility.php b/system/database/drivers/oci8/oci8_utility.php
new file mode 100644
index 0000000..bcce114
--- /dev/null
+++ b/system/database/drivers/oci8/oci8_utility.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.4.1
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Oracle Utility Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_oci8_utility extends CI_DB_utility {
+
+	/**
+	 * List databases statement
+	 *
+	 * @var	string
+	 */
+	protected $_list_databases	= 'SELECT username FROM dba_users'; // Schemas are actual usernames
+
+	/**
+	 * Export
+	 *
+	 * @param	array	$params	Preferences
+	 * @return	mixed
+	 */
+	protected function _backup($params = array())
+	{
+		// Currently unsupported
+		return $this->db->display_error('db_unsupported_feature');
+	}
+
+}
diff --git a/system/database/drivers/odbc/index.html b/system/database/drivers/odbc/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/drivers/odbc/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/system/database/drivers/odbc/odbc_driver.php b/system/database/drivers/odbc/odbc_driver.php
new file mode 100644
index 0000000..cfb9d57
--- /dev/null
+++ b/system/database/drivers/odbc/odbc_driver.php
@@ -0,0 +1,426 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * ODBC Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_odbc_driver extends CI_DB_driver {
+
+	/**
+	 * Database driver
+	 *
+	 * @var	string
+	 */
+	public $dbdriver = 'odbc';
+
+	/**
+	 * Database schema
+	 *
+	 * @var	string
+	 */
+	public $schema = 'public';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Identifier escape character
+	 *
+	 * Must be empty for ODBC.
+	 *
+	 * @var	string
+	 */
+	protected $_escape_char = '';
+
+	/**
+	 * ESCAPE statement string
+	 *
+	 * @var	string
+	 */
+	protected $_like_escape_str = " {escape '%s'} ";
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('RND()', 'RND(%d)');
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ODBC result ID resource returned from odbc_prepare()
+	 *
+	 * @var	resource
+	 */
+	private $odbc_result;
+
+	/**
+	 * Values to use with odbc_execute() for prepared statements
+	 *
+	 * @var	array
+	 */
+	private $binds = array();
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		// Legacy support for DSN in the hostname field
+		if (empty($this->dsn))
+		{
+			$this->dsn = $this->hostname;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Non-persistent database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	resource
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		return ($persistent === TRUE)
+			? odbc_pconnect($this->dsn, $this->username, $this->password)
+			: odbc_connect($this->dsn, $this->username, $this->password);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Compile Bindings
+	 *
+	 * @param	string	$sql	SQL statement
+	 * @param	array	$binds	An array of values to bind
+	 * @return	string
+	 */
+	public function compile_binds($sql, $binds)
+	{
+		if (empty($binds) OR empty($this->bind_marker) OR strpos($sql, $this->bind_marker) === FALSE)
+		{
+			return $sql;
+		}
+		elseif ( ! is_array($binds))
+		{
+			$binds = array($binds);
+			$bind_count = 1;
+		}
+		else
+		{
+			// Make sure we're using numeric keys
+			$binds = array_values($binds);
+			$bind_count = count($binds);
+		}
+
+		// We'll need the marker length later
+		$ml = strlen($this->bind_marker);
+
+		// Make sure not to replace a chunk inside a string that happens to match the bind marker
+		if ($c = preg_match_all("/'[^']*'|\"[^\"]*\"/i", $sql, $matches))
+		{
+			$c = preg_match_all('/'.preg_quote($this->bind_marker, '/').'/i',
+				str_replace($matches[0],
+					str_replace($this->bind_marker, str_repeat(' ', $ml), $matches[0]),
+					$sql, $c),
+				$matches, PREG_OFFSET_CAPTURE);
+
+			// Bind values' count must match the count of markers in the query
+			if ($bind_count !== $c)
+			{
+				return $sql;
+			}
+		}
+		elseif (($c = preg_match_all('/'.preg_quote($this->bind_marker, '/').'/i', $sql, $matches, PREG_OFFSET_CAPTURE)) !== $bind_count)
+		{
+			return $sql;
+		}
+
+		if ($this->bind_marker !== '?')
+		{
+			do
+			{
+				$c--;
+				$sql = substr_replace($sql, '?', $matches[0][$c][1], $ml);
+			}
+			while ($c !== 0);
+		}
+
+		if (FALSE !== ($this->odbc_result = odbc_prepare($this->conn_id, $sql)))
+		{
+			$this->binds = array_values($binds);
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Execute the query
+	 *
+	 * @param	string	$sql	an SQL query
+	 * @return	resource
+	 */
+	protected function _execute($sql)
+	{
+		if ( ! isset($this->odbc_result))
+		{
+			return odbc_exec($this->conn_id, $sql);
+		}
+		elseif ($this->odbc_result === FALSE)
+		{
+			return FALSE;
+		}
+
+		if (TRUE === ($success = odbc_execute($this->odbc_result, $this->binds)))
+		{
+			// For queries that return result sets, return the result_id resource on success
+			$this->is_write_type($sql) OR $success = $this->odbc_result;
+		}
+
+		$this->odbc_result = NULL;
+		$this->binds       = array();
+
+		return $success;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Begin Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_begin()
+	{
+		return odbc_autocommit($this->conn_id, FALSE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Commit Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_commit()
+	{
+		if (odbc_commit($this->conn_id))
+		{
+			odbc_autocommit($this->conn_id, TRUE);
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rollback Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_rollback()
+	{
+		if (odbc_rollback($this->conn_id))
+		{
+			odbc_autocommit($this->conn_id, TRUE);
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Determines if a query is a "write" type.
+	 *
+	 * @param	string	An SQL query string
+	 * @return	bool
+	 */
+	public function is_write_type($sql)
+	{
+		if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql))
+		{
+			return FALSE;
+		}
+
+		return parent::is_write_type($sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Platform-dependent string escape
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _escape_str($str)
+	{
+		$this->display_error('db_unsupported_feature');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Affected Rows
+	 *
+	 * @return	int
+	 */
+	public function affected_rows()
+	{
+		return odbc_num_rows($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert ID
+	 *
+	 * @return	bool
+	 */
+	public function insert_id()
+	{
+		return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = '".$this->schema."'";
+
+		if ($prefix_limit !== FALSE && $this->dbprefix !== '')
+		{
+			return $sql." AND table_name LIKE '".$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SHOW COLUMNS FROM '.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data query
+	 *
+	 * Generates a platform-specific query so that the column data can be retrieved
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _field_data($table)
+	{
+		return 'SELECT TOP 1 FROM '.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error
+	 *
+	 * Returns an array containing code and message of the last
+	 * database error that has occurred.
+	 *
+	 * @return	array
+	 */
+	public function error()
+	{
+		return array('code' => odbc_error($this->conn_id), 'message' => odbc_errormsg($this->conn_id));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close DB Connection
+	 *
+	 * @return	void
+	 */
+	protected function _close()
+	{
+		odbc_close($this->conn_id);
+	}
+}
diff --git a/system/database/drivers/odbc/odbc_forge.php b/system/database/drivers/odbc/odbc_forge.php
new file mode 100644
index 0000000..115d08a
--- /dev/null
+++ b/system/database/drivers/odbc/odbc_forge.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * ODBC Forge Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/database/
+ */
+class CI_DB_odbc_forge extends CI_DB_forge {
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= FALSE;
+
+	/**
+	 * DROP TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_table_if	= FALSE;
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	bool|array
+	 */
+	protected $_unsigned		= FALSE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		// Not supported (in most databases at least)
+	}
+
+}
diff --git a/system/database/drivers/odbc/odbc_result.php b/system/database/drivers/odbc/odbc_result.php
new file mode 100644
index 0000000..e5847f1
--- /dev/null
+++ b/system/database/drivers/odbc/odbc_result.php
@@ -0,0 +1,269 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * ODBC Result Class
+ *
+ * This class extends the parent result class: CI_DB_result
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_odbc_result extends CI_DB_result {
+
+	/**
+	 * Number of rows in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_rows()
+	{
+		if (is_int($this->num_rows))
+		{
+			return $this->num_rows;
+		}
+		elseif (($this->num_rows = odbc_num_rows($this->result_id)) !== -1)
+		{
+			return $this->num_rows;
+		}
+
+		// Work-around for ODBC subdrivers that don't support num_rows()
+		if (count($this->result_array) > 0)
+		{
+			return $this->num_rows = count($this->result_array);
+		}
+		elseif (count($this->result_object) > 0)
+		{
+			return $this->num_rows = count($this->result_object);
+		}
+
+		return $this->num_rows = count($this->result_array());
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Number of fields in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_fields()
+	{
+		return odbc_num_fields($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * Generates an array of column names
+	 *
+	 * @return	array
+	 */
+	public function list_fields()
+	{
+		$field_names = array();
+		$num_fields = $this->num_fields();
+
+		if ($num_fields > 0)
+		{
+			for ($i = 1; $i <= $num_fields; $i++)
+			{
+				$field_names[] = odbc_field_name($this->result_id, $i);
+			}
+		}
+
+		return $field_names;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data
+	 *
+	 * Generates an array of objects containing field meta-data
+	 *
+	 * @return	array
+	 */
+	public function field_data()
+	{
+		$retval = array();
+		for ($i = 0, $odbc_index = 1, $c = $this->num_fields(); $i < $c; $i++, $odbc_index++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= odbc_field_name($this->result_id, $odbc_index);
+			$retval[$i]->type		= odbc_field_type($this->result_id, $odbc_index);
+			$retval[$i]->max_length		= odbc_field_len($this->result_id, $odbc_index);
+			$retval[$i]->primary_key	= 0;
+			$retval[$i]->default		= '';
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Free the result
+	 *
+	 * @return	void
+	 */
+	public function free_result()
+	{
+		if (is_resource($this->result_id))
+		{
+			odbc_free_result($this->result_id);
+			$this->result_id = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - associative array
+	 *
+	 * Returns the result set as an array
+	 *
+	 * @return	array
+	 */
+	protected function _fetch_assoc()
+	{
+		return odbc_fetch_array($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - object
+	 *
+	 * Returns the result set as an object
+	 *
+	 * @param	string	$class_name
+	 * @return	object
+	 */
+	protected function _fetch_object($class_name = 'stdClass')
+	{
+		$row = odbc_fetch_object($this->result_id);
+
+		if ($class_name === 'stdClass' OR ! $row)
+		{
+			return $row;
+		}
+
+		$class_name = new $class_name();
+		foreach ($row as $key => $value)
+		{
+			$class_name->$key = $value;
+		}
+
+		return $class_name;
+	}
+
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('odbc_fetch_array'))
+{
+	/**
+	 * ODBC Fetch array
+	 *
+	 * Emulates the native odbc_fetch_array() function when
+	 * it is not available (odbc_fetch_array() requires unixODBC)
+	 *
+	 * @param	resource	&$result
+	 * @param	int		$rownumber
+	 * @return	array
+	 */
+	function odbc_fetch_array(&$result, $rownumber = 1)
+	{
+		$rs = array();
+		if ( ! odbc_fetch_into($result, $rs, $rownumber))
+		{
+			return FALSE;
+		}
+
+		$rs_assoc = array();
+		foreach ($rs as $k => $v)
+		{
+			$field_name = odbc_field_name($result, $k+1);
+			$rs_assoc[$field_name] = $v;
+		}
+
+		return $rs_assoc;
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('odbc_fetch_object'))
+{
+	/**
+	 * ODBC Fetch object
+	 *
+	 * Emulates the native odbc_fetch_object() function when
+	 * it is not available.
+	 *
+	 * @param	resource	&$result
+	 * @param	int		$rownumber
+	 * @return	object
+	 */
+	function odbc_fetch_object(&$result, $rownumber = 1)
+	{
+		$rs = array();
+		if ( ! odbc_fetch_into($result, $rs, $rownumber))
+		{
+			return FALSE;
+		}
+
+		$rs_object = new stdClass();
+		foreach ($rs as $k => $v)
+		{
+			$field_name = odbc_field_name($result, $k+1);
+			$rs_object->$field_name = $v;
+		}
+
+		return $rs_object;
+	}
+}
diff --git a/system/database/drivers/odbc/odbc_utility.php b/system/database/drivers/odbc/odbc_utility.php
new file mode 100644
index 0000000..a69ed00
--- /dev/null
+++ b/system/database/drivers/odbc/odbc_utility.php
@@ -0,0 +1,64 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * ODBC Utility Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/database/
+ */
+class CI_DB_odbc_utility extends CI_DB_utility {
+
+	/**
+	 * Export
+	 *
+	 * @param	array	$params	Preferences
+	 * @return	mixed
+	 */
+	protected function _backup($params = array())
+	{
+		// Currently unsupported
+		return $this->db->display_error('db_unsupported_feature');
+	}
+
+}
diff --git a/system/database/drivers/pdo/index.html b/system/database/drivers/pdo/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/drivers/pdo/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/system/database/drivers/pdo/pdo_driver.php b/system/database/drivers/pdo/pdo_driver.php
new file mode 100644
index 0000000..559e865
--- /dev/null
+++ b/system/database/drivers/pdo/pdo_driver.php
@@ -0,0 +1,351 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 2.1.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_driver extends CI_DB {
+
+	/**
+	 * Database driver
+	 *
+	 * @var	string
+	 */
+	public $dbdriver = 'pdo';
+
+	/**
+	 * PDO Options
+	 *
+	 * @var	array
+	 */
+	public $options = array();
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Validates the DSN string and/or detects the subdriver.
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if (preg_match('/([^:]+):/', $this->dsn, $match) && count($match) === 2)
+		{
+			// If there is a minimum valid dsn string pattern found, we're done
+			// This is for general PDO users, who tend to have a full DSN string.
+			$this->subdriver = $match[1];
+			return;
+		}
+		// Legacy support for DSN specified in the hostname field
+		elseif (preg_match('/([^:]+):/', $this->hostname, $match) && count($match) === 2)
+		{
+			$this->dsn = $this->hostname;
+			$this->hostname = NULL;
+			$this->subdriver = $match[1];
+			return;
+		}
+		elseif (in_array($this->subdriver, array('mssql', 'sybase'), TRUE))
+		{
+			$this->subdriver = 'dblib';
+		}
+		elseif ($this->subdriver === '4D')
+		{
+			$this->subdriver = '4d';
+		}
+		elseif ( ! in_array($this->subdriver, array('4d', 'cubrid', 'dblib', 'firebird', 'ibm', 'informix', 'mysql', 'oci', 'odbc', 'pgsql', 'sqlite', 'sqlsrv'), TRUE))
+		{
+			log_message('error', 'PDO: Invalid or non-existent subdriver');
+
+			if ($this->db_debug)
+			{
+				show_error('Invalid or non-existent PDO subdriver');
+			}
+		}
+
+		$this->dsn = NULL;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	object
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		if ($persistent === TRUE)
+		{
+			$this->options[PDO::ATTR_PERSISTENT] = TRUE;
+		}
+
+		// From PHP8.0, default PDO::ATTR_ERRMODE is changed
+		// from PDO::ERRMODE_SILENT to PDO::ERRMODE_EXCEPTION
+		// as https://wiki.php.net/rfc/pdo_default_errmode
+		if ( ! isset($this->options[PDO::ATTR_ERRMODE]))
+		{
+			$this->options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_SILENT;
+		}
+
+		try
+		{
+			return new PDO($this->dsn, $this->username, $this->password, $this->options);
+		}
+		catch (PDOException $e)
+		{
+			if ($this->db_debug && empty($this->failover))
+			{
+				$this->display_error($e->getMessage(), '', TRUE);
+			}
+
+			return FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database version number
+	 *
+	 * @return	string
+	 */
+	public function version()
+	{
+		if (isset($this->data_cache['version']))
+		{
+			return $this->data_cache['version'];
+		}
+
+		// Not all subdrivers support the getAttribute() method
+		try
+		{
+			return $this->data_cache['version'] = $this->conn_id->getAttribute(PDO::ATTR_SERVER_VERSION);
+		}
+		catch (PDOException $e)
+		{
+			return parent::version();
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Execute the query
+	 *
+	 * @param	string	$sql	SQL query
+	 * @return	mixed
+	 */
+	protected function _execute($sql)
+	{
+		return $this->conn_id->query($sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Begin Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_begin()
+	{
+		return $this->conn_id->beginTransaction();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Commit Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_commit()
+	{
+		return $this->conn_id->commit();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rollback Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_rollback()
+	{
+		return $this->conn_id->rollBack();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Platform-dependent string escape
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _escape_str($str)
+	{
+		// Escape the string
+		$str = $this->conn_id->quote($str);
+
+		// If there are duplicated quotes, trim them away
+		return ($str[0] === "'")
+			? substr($str, 1, -1)
+			: $str;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Affected Rows
+	 *
+	 * @return	int
+	 */
+	public function affected_rows()
+	{
+		return is_object($this->result_id) ? $this->result_id->rowCount() : 0;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert ID
+	 *
+	 * @param	string	$name
+	 * @return	int
+	 */
+	public function insert_id($name = NULL)
+	{
+		return $this->conn_id->lastInsertId($name);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data query
+	 *
+	 * Generates a platform-specific query so that the column data can be retrieved
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _field_data($table)
+	{
+		return 'SELECT TOP 1 * FROM '.$this->protect_identifiers($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error
+	 *
+	 * Returns an array containing code and message of the last
+	 * database error that has occurred.
+	 *
+	 * @return	array
+	 */
+	public function error()
+	{
+		$error = array('code' => '00000', 'message' => '');
+		$pdo_error = $this->conn_id->errorInfo();
+
+		if (empty($pdo_error[0]))
+		{
+			return $error;
+		}
+
+		$error['code'] = isset($pdo_error[1]) ? $pdo_error[0].'/'.$pdo_error[1] : $pdo_error[0];
+		if (isset($pdo_error[2]))
+		{
+			$error['message'] = $pdo_error[2];
+		}
+
+		return $error;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Truncate statement
+	 *
+	 * Generates a platform-specific truncate string from the supplied data
+	 *
+	 * If the database does not support the TRUNCATE statement,
+	 * then this method maps to 'DELETE FROM table'
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _truncate($table)
+	{
+		return 'TRUNCATE TABLE '.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close DB Connection
+	 *
+	 * @return	void
+	 */
+	protected function _close()
+	{
+		$this->result_id = FALSE;
+		$this->conn_id = FALSE;
+	}
+
+}
diff --git a/system/database/drivers/pdo/pdo_forge.php b/system/database/drivers/pdo/pdo_forge.php
new file mode 100644
index 0000000..b35ff67
--- /dev/null
+++ b/system/database/drivers/pdo/pdo_forge.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 2.1.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO Forge Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/database/
+ */
+class CI_DB_pdo_forge extends CI_DB_forge {
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= FALSE;
+
+	/**
+	 * DROP TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_table_if	= FALSE;
+
+}
diff --git a/system/database/drivers/pdo/pdo_result.php b/system/database/drivers/pdo/pdo_result.php
new file mode 100644
index 0000000..bf9e123
--- /dev/null
+++ b/system/database/drivers/pdo/pdo_result.php
@@ -0,0 +1,199 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 2.1.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO Result Class
+ *
+ * This class extends the parent result class: CI_DB_result
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_result extends CI_DB_result {
+
+	/**
+	 * Number of rows in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_rows()
+	{
+		if (is_int($this->num_rows))
+		{
+			return $this->num_rows;
+		}
+		elseif (count($this->result_array) > 0)
+		{
+			return $this->num_rows = count($this->result_array);
+		}
+		elseif (count($this->result_object) > 0)
+		{
+			return $this->num_rows = count($this->result_object);
+		}
+		elseif (($num_rows = $this->result_id->rowCount()) > 0)
+		{
+			return $this->num_rows = $num_rows;
+		}
+
+		return $this->num_rows = count($this->result_array());
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Number of fields in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_fields()
+	{
+		return $this->result_id->columnCount();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * Generates an array of column names
+	 *
+	 * @return	bool
+	 */
+	public function list_fields()
+	{
+		$field_names = array();
+		for ($i = 0, $c = $this->num_fields(); $i < $c; $i++)
+		{
+			// Might trigger an E_WARNING due to not all subdrivers
+			// supporting getColumnMeta()
+			$field_names[$i] = @$this->result_id->getColumnMeta($i);
+			$field_names[$i] = $field_names[$i]['name'];
+		}
+
+		return $field_names;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data
+	 *
+	 * Generates an array of objects containing field meta-data
+	 *
+	 * @return	array
+	 */
+	public function field_data()
+	{
+		try
+		{
+			$retval = array();
+
+			for ($i = 0, $c = $this->num_fields(); $i < $c; $i++)
+			{
+				$field = $this->result_id->getColumnMeta($i);
+
+				$retval[$i]			= new stdClass();
+				$retval[$i]->name		= $field['name'];
+				$retval[$i]->type		= isset($field['native_type']) ? $field['native_type'] : null;
+				$retval[$i]->max_length		= ($field['len'] > 0) ? $field['len'] : NULL;
+				$retval[$i]->primary_key	= (int) ( ! empty($field['flags']) && in_array('primary_key', $field['flags'], TRUE));
+			}
+
+			return $retval;
+		}
+		catch (Exception $e)
+		{
+			if ($this->db->db_debug)
+			{
+				return $this->db->display_error('db_unsupported_feature');
+			}
+
+			return FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Free the result
+	 *
+	 * @return	void
+	 */
+	public function free_result()
+	{
+		if (is_object($this->result_id))
+		{
+			$this->result_id = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - associative array
+	 *
+	 * Returns the result set as an array
+	 *
+	 * @return	array
+	 */
+	protected function _fetch_assoc()
+	{
+		return $this->result_id->fetch(PDO::FETCH_ASSOC);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - object
+	 *
+	 * Returns the result set as an object
+	 *
+	 * @param	string	$class_name
+	 * @return	object
+	 */
+	protected function _fetch_object($class_name = 'stdClass')
+	{
+		return $this->result_id->fetchObject($class_name);
+	}
+
+}
diff --git a/system/database/drivers/pdo/pdo_utility.php b/system/database/drivers/pdo/pdo_utility.php
new file mode 100644
index 0000000..2094ef4
--- /dev/null
+++ b/system/database/drivers/pdo/pdo_utility.php
@@ -0,0 +1,64 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 2.1.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO Utility Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/database/
+ */
+class CI_DB_pdo_utility extends CI_DB_utility {
+
+	/**
+	 * Export
+	 *
+	 * @param	array	$params	Preferences
+	 * @return	mixed
+	 */
+	protected function _backup($params = array())
+	{
+		// Currently unsupported
+		return $this->db->display_error('db_unsupported_feature');
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/index.html b/system/database/drivers/pdo/subdrivers/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/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/system/database/drivers/pdo/subdrivers/pdo_4d_driver.php b/system/database/drivers/pdo/subdrivers/pdo_4d_driver.php
new file mode 100644
index 0000000..8d5b2f6
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_4d_driver.php
@@ -0,0 +1,201 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO 4D Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_4d_driver extends CI_DB_pdo_driver {
+
+	/**
+	 * Sub-driver
+	 *
+	 * @var	string
+	 */
+	public $subdriver = '4d';
+
+	/**
+	 * Identifier escape character
+	 *
+	 * @var	string[]
+	 */
+	protected $_escape_char = array('[', ']');
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Builds the DSN if not already set.
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if (empty($this->dsn))
+		{
+			$this->dsn = '4D:host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname);
+
+			empty($this->port) OR $this->dsn .= ';port='.$this->port;
+			empty($this->database) OR $this->dsn .= ';dbname='.$this->database;
+			empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set;
+		}
+		elseif ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 3) === FALSE)
+		{
+			$this->dsn .= ';charset='.$this->char_set;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SELECT '.$this->escape_identifiers('TABLE_NAME').' FROM '.$this->escape_identifiers('_USER_TABLES');
+
+		if ($prefix_limit === TRUE && $this->dbprefix !== '')
+		{
+			$sql .= ' WHERE '.$this->escape_identifiers('TABLE_NAME')." LIKE '".$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SELECT '.$this->escape_identifiers('COLUMN_NAME').' FROM '.$this->escape_identifiers('_USER_COLUMNS')
+			.' WHERE '.$this->escape_identifiers('TABLE_NAME').' = '.$this->escape($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data query
+	 *
+	 * Generates a platform-specific query so that the column data can be retrieved
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _field_data($table)
+	{
+		return 'SELECT * FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE).' LIMIT 1';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update statement
+	 *
+	 * Generates a platform-specific update string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @param	array	$values
+	 * @return	string
+	 */
+	protected function _update($table, $values)
+	{
+		$this->qb_limit = FALSE;
+		$this->qb_orderby = array();
+		return parent::_update($table, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete statement
+	 *
+	 * Generates a platform-specific delete string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _delete($table)
+	{
+		$this->qb_limit = FALSE;
+		return parent::_delete($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT
+	 *
+	 * Generates a platform-specific LIMIT clause
+	 *
+	 * @param	string	$sql	SQL Query
+	 * @return	string
+	 */
+	protected function _limit($sql)
+	{
+		return $sql.' LIMIT '.$this->qb_limit.($this->qb_offset ? ' OFFSET '.$this->qb_offset : '');
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_4d_forge.php b/system/database/drivers/pdo/subdrivers/pdo_4d_forge.php
new file mode 100644
index 0000000..28fc008
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_4d_forge.php
@@ -0,0 +1,218 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO 4D Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_4d_forge extends CI_DB_pdo_forge {
+
+	/**
+	 * CREATE DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_database	= 'CREATE SCHEMA %s';
+
+	/**
+	 * DROP DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_database	= 'DROP SCHEMA %s';
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= 'CREATE TABLE IF NOT EXISTS';
+
+	/**
+	 * RENAME TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_rename_table	= FALSE;
+
+	/**
+	 * DROP TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_table_if	= 'DROP TABLE IF EXISTS';
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'INT16'		=> 'INT',
+		'SMALLINT'	=> 'INT',
+		'INT'		=> 'INT64',
+		'INT32'		=> 'INT64'
+	);
+
+	/**
+	 * DEFAULT value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_default		= FALSE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if (in_array($alter_type, array('ADD', 'DROP'), TRUE))
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+
+		// No method of modifying columns is supported
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process column
+	 *
+	 * @param	array	$field
+	 * @return	string
+	 */
+	protected function _process_column($field)
+	{
+		return $this->db->escape_identifiers($field['name'])
+			.' '.$field['type'].$field['length']
+			.$field['null']
+			.$field['unique']
+			.$field['auto_increment'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'TINYINT':
+				$attributes['TYPE'] = 'SMALLINT';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'INTEGER';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'INTEGER':
+				$attributes['TYPE'] = 'INT';
+				return;
+			case 'BIGINT':
+				$attributes['TYPE'] = 'INT64';
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute UNIQUE
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_unique(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === TRUE)
+		{
+			$field['unique'] = ' UNIQUE';
+
+			// UNIQUE must be used with NOT NULL
+			$field['null'] = ' NOT NULL';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE)
+		{
+			if (stripos($field['type'], 'int') !== FALSE)
+			{
+				$field['auto_increment'] = ' AUTO_INCREMENT';
+			}
+			elseif (strcasecmp($field['type'], 'UUID') === 0)
+			{
+				$field['auto_increment'] = ' AUTO_GENERATE';
+			}
+		}
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_cubrid_driver.php b/system/database/drivers/pdo/subdrivers/pdo_cubrid_driver.php
new file mode 100644
index 0000000..c8f9258
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_cubrid_driver.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) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO CUBRID Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_cubrid_driver extends CI_DB_pdo_driver {
+
+	/**
+	 * Sub-driver
+	 *
+	 * @var	string
+	 */
+	public $subdriver = 'cubrid';
+
+	/**
+	 * Identifier escape character
+	 *
+	 * @var	string
+	 */
+	protected $_escape_char = '`';
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var array
+	 */
+	protected $_random_keyword = array('RANDOM()', 'RANDOM(%d)');
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Builds the DSN if not already set.
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if (empty($this->dsn))
+		{
+			$this->dsn = 'cubrid:host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname);
+
+			empty($this->port) OR $this->dsn .= ';port='.$this->port;
+			empty($this->database) OR $this->dsn .= ';dbname='.$this->database;
+			empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SHOW TABLES';
+
+		if ($prefix_limit === TRUE && $this->dbprefix !== '')
+		{
+			return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'";
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE)
+		{
+			return FALSE;
+		}
+		$query = $query->result_object();
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]->Field;
+
+			sscanf($query[$i]->Type, '%[a-z](%d)',
+				$retval[$i]->type,
+				$retval[$i]->max_length
+			);
+
+			$retval[$i]->default		= $query[$i]->Default;
+			$retval[$i]->primary_key	= (int) ($query[$i]->Key === 'PRI');
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Truncate statement
+	 *
+	 * Generates a platform-specific truncate string from the supplied data
+	 *
+	 * If the database does not support the TRUNCATE statement,
+	 * then this method maps to 'DELETE FROM table'
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _truncate($table)
+	{
+		return 'TRUNCATE '.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * FROM tables
+	 *
+	 * Groups tables in FROM clauses if needed, so there is no confusion
+	 * about operator precedence.
+	 *
+	 * @return	string
+	 */
+	protected function _from_tables()
+	{
+		if ( ! empty($this->qb_join) && count($this->qb_from) > 1)
+		{
+			return '('.implode(', ', $this->qb_from).')';
+		}
+
+		return implode(', ', $this->qb_from);
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_cubrid_forge.php b/system/database/drivers/pdo/subdrivers/pdo_cubrid_forge.php
new file mode 100644
index 0000000..de02983
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_cubrid_forge.php
@@ -0,0 +1,231 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO CUBRID Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_cubrid_forge extends CI_DB_pdo_forge {
+
+	/**
+	 * CREATE DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_database	= FALSE;
+
+	/**
+	 * DROP DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_database	= FALSE;
+
+	/**
+	 * CREATE TABLE keys flag
+	 *
+	 * Whether table keys are created from within the
+	 * CREATE TABLE statement.
+	 *
+	 * @var	bool
+	 */
+	protected $_create_table_keys	= TRUE;
+
+	/**
+	 * DROP TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_table_if	= 'DROP TABLE IF EXISTS';
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'SHORT'		=> 'INTEGER',
+		'SMALLINT'	=> 'INTEGER',
+		'INT'		=> 'BIGINT',
+		'INTEGER'	=> 'BIGINT',
+		'BIGINT'	=> 'NUMERIC',
+		'FLOAT'		=> 'DOUBLE',
+		'REAL'		=> 'DOUBLE'
+	);
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if (in_array($alter_type, array('DROP', 'ADD'), TRUE))
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table);
+		$sqls = array();
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			if ($field[$i]['_literal'] !== FALSE)
+			{
+				$sqls[] = $sql.' CHANGE '.$field[$i]['_literal'];
+			}
+			else
+			{
+				$alter_type = empty($field[$i]['new_name']) ? ' MODIFY ' : ' CHANGE ';
+				$sqls[] = $sql.$alter_type.$this->_process_column($field[$i]);
+			}
+		}
+
+		return $sqls;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process column
+	 *
+	 * @param	array	$field
+	 * @return	string
+	 */
+	protected function _process_column($field)
+	{
+		$extra_clause = isset($field['after'])
+			? ' AFTER '.$this->db->escape_identifiers($field['after']) : '';
+
+		if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE)
+		{
+			$extra_clause = ' FIRST';
+		}
+
+		return $this->db->escape_identifiers($field['name'])
+			.(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name']))
+			.' '.$field['type'].$field['length']
+			.$field['unsigned']
+			.$field['null']
+			.$field['default']
+			.$field['auto_increment']
+			.$field['unique']
+			.$extra_clause;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'TINYINT':
+				$attributes['TYPE'] = 'SMALLINT';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'INTEGER';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'LONGTEXT':
+				$attributes['TYPE'] = 'STRING';
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process indexes
+	 *
+	 * @param	string	$table	(ignored)
+	 * @return	string
+	 */
+	protected function _process_indexes($table)
+	{
+		$sql = '';
+
+		for ($i = 0, $c = count($this->keys); $i < $c; $i++)
+		{
+			if (is_array($this->keys[$i]))
+			{
+				for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++)
+				{
+					if ( ! isset($this->fields[$this->keys[$i][$i2]]))
+					{
+						unset($this->keys[$i][$i2]);
+						continue;
+					}
+				}
+			}
+			elseif ( ! isset($this->fields[$this->keys[$i]]))
+			{
+				unset($this->keys[$i]);
+				continue;
+			}
+
+			is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]);
+
+			$sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i]))
+				.' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')';
+		}
+
+		$this->keys = array();
+
+		return $sql;
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_dblib_driver.php b/system/database/drivers/pdo/subdrivers/pdo_dblib_driver.php
new file mode 100644
index 0000000..7d8d4a2
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_dblib_driver.php
@@ -0,0 +1,354 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO DBLIB Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_dblib_driver extends CI_DB_pdo_driver {
+
+	/**
+	 * Sub-driver
+	 *
+	 * @var	string
+	 */
+	public $subdriver = 'dblib';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('NEWID()', 'RAND(%d)');
+
+	/**
+	 * Quoted identifier flag
+	 *
+	 * Whether to use SQL-92 standard quoted identifier
+	 * (double quotes) or brackets for identifier escaping.
+	 *
+	 * @var	bool
+	 */
+	protected $_quoted_identifier;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Builds the DSN if not already set.
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if (empty($this->dsn))
+		{
+			$this->dsn = $params['subdriver'].':host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname);
+
+			if ( ! empty($this->port))
+			{
+				$this->dsn .= (DIRECTORY_SEPARATOR === '\\' ? ',' : ':').$this->port;
+			}
+
+			empty($this->database) OR $this->dsn .= ';dbname='.$this->database;
+			empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set;
+			empty($this->appname) OR $this->dsn .= ';appname='.$this->appname;
+		}
+		else
+		{
+			if ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 6) === FALSE)
+			{
+				$this->dsn .= ';charset='.$this->char_set;
+			}
+
+			$this->subdriver = 'dblib';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	object
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		if ($persistent === TRUE)
+		{
+			log_message('debug', "dblib driver doesn't support persistent connections");
+		}
+
+		$this->conn_id = parent::db_connect(FALSE);
+
+		if ( ! is_object($this->conn_id))
+		{
+			return $this->conn_id;
+		}
+
+		// Determine how identifiers are escaped
+		$query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi');
+		$query = $query->row_array();
+		$this->_quoted_identifier = empty($query) ? FALSE : (bool) $query['qi'];
+		$this->_escape_char = ($this->_quoted_identifier) ? '"' : array('[', ']');
+
+		return $this->conn_id;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SELECT '.$this->escape_identifiers('name')
+			.' FROM '.$this->escape_identifiers('sysobjects')
+			.' WHERE '.$this->escape_identifiers('type')." = 'U'";
+
+		if ($prefix_limit === TRUE && $this->dbprefix !== '')
+		{
+			$sql .= ' AND '.$this->escape_identifiers('name')." LIKE '".$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql.' ORDER BY '.$this->escape_identifiers('name');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SELECT COLUMN_NAME
+			FROM INFORMATION_SCHEMA.Columns
+			WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		$sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT
+			FROM INFORMATION_SCHEMA.Columns
+			WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
+
+		if (($query = $this->query($sql)) === FALSE)
+		{
+			return FALSE;
+		}
+		$query = $query->result_object();
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]->COLUMN_NAME;
+			$retval[$i]->type		= $query[$i]->DATA_TYPE;
+			$retval[$i]->max_length		= ($query[$i]->CHARACTER_MAXIMUM_LENGTH > 0) ? $query[$i]->CHARACTER_MAXIMUM_LENGTH : $query[$i]->NUMERIC_PRECISION;
+			$retval[$i]->default		= $query[$i]->COLUMN_DEFAULT;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update statement
+	 *
+	 * Generates a platform-specific update string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @param	array	$values
+	 * @return	string
+	 */
+	protected function _update($table, $values)
+	{
+		$this->qb_limit = FALSE;
+		$this->qb_orderby = array();
+		return parent::_update($table, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete statement
+	 *
+	 * Generates a platform-specific delete string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _delete($table)
+	{
+		if ($this->qb_limit)
+		{
+			return 'WITH ci_delete AS (SELECT TOP '.$this->qb_limit.' * FROM '.$table.$this->_compile_wh('qb_where').') DELETE FROM ci_delete';
+		}
+
+		return parent::_delete($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT
+	 *
+	 * Generates a platform-specific LIMIT clause
+	 *
+	 * @param	string	$sql	SQL Query
+	 * @return	string
+	 */
+	protected function _limit($sql)
+	{
+		$limit = $this->qb_offset + $this->qb_limit;
+
+		// As of SQL Server 2005 (9.0.*) ROW_NUMBER() is supported,
+		// however an ORDER BY clause is required for it to work
+		if (version_compare($this->version(), '9', '>=') && $this->qb_offset && ! empty($this->qb_orderby))
+		{
+			$orderby = $this->_compile_order_by();
+
+			// We have to strip the ORDER BY clause
+			$sql = trim(substr($sql, 0, strrpos($sql, $orderby)));
+
+			// Get the fields to select from our subquery, so that we can avoid CI_rownum appearing in the actual results
+			if (count($this->qb_select) === 0 OR strpos(implode(',', $this->qb_select), '*') !== FALSE)
+			{
+				$select = '*'; // Inevitable
+			}
+			else
+			{
+				// Use only field names and their aliases, everything else is out of our scope.
+				$select = array();
+				$field_regexp = ($this->_quoted_identifier)
+					? '("[^\"]+")' : '(\[[^\]]+\])';
+				for ($i = 0, $c = count($this->qb_select); $i < $c; $i++)
+				{
+					$select[] = preg_match('/(?:\s|\.)'.$field_regexp.'$/i', $this->qb_select[$i], $m)
+						? $m[1] : $this->qb_select[$i];
+				}
+				$select = implode(', ', $select);
+			}
+
+			return 'SELECT '.$select." FROM (\n\n"
+				.preg_replace('/^(SELECT( DISTINCT)?)/i', '\\1 ROW_NUMBER() OVER('.trim($orderby).') AS '.$this->escape_identifiers('CI_rownum').', ', $sql)
+				."\n\n) ".$this->escape_identifiers('CI_subquery')
+				."\nWHERE ".$this->escape_identifiers('CI_rownum').' BETWEEN '.($this->qb_offset + 1).' AND '.$limit;
+		}
+
+		return preg_replace('/(^\SELECT (DISTINCT)?)/i','\\1 TOP '.$limit.' ', $sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert batch statement
+	 *
+	 * Generates a platform-specific insert string from the supplied data.
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$keys	INSERT keys
+	 * @param	array	$values	INSERT values
+	 * @return	string|bool
+	 */
+	protected function _insert_batch($table, $keys, $values)
+	{
+		// Multiple-value inserts are only supported as of SQL Server 2008
+		if (version_compare($this->version(), '10', '>='))
+		{
+			return parent::_insert_batch($table, $keys, $values);
+		}
+
+		return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database version number
+	 *
+	 * @return      string
+	 */
+	public function version()
+	{
+		if (isset($this->data_cache['version']))
+		{
+			return $this->data_cache['version'];
+		}
+
+		return $this->data_cache['version'] = $this->conn_id->query("SELECT SERVERPROPERTY('ProductVersion') AS ver")->fetchColumn(0);
+	}
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_dblib_forge.php b/system/database/drivers/pdo/subdrivers/pdo_dblib_forge.php
new file mode 100644
index 0000000..3ee352f
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_dblib_forge.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO DBLIB Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_dblib_forge extends CI_DB_pdo_forge {
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= "IF NOT EXISTS (SELECT * FROM sysobjects WHERE ID = object_id(N'%s') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)\nCREATE TABLE";
+
+	/**
+	 * DROP TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_table_if	= "IF EXISTS (SELECT * FROM sysobjects WHERE ID = object_id(N'%s') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)\nDROP TABLE";
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'TINYINT'	=> 'SMALLINT',
+		'SMALLINT'	=> 'INT',
+		'INT'		=> 'BIGINT',
+		'REAL'		=> 'FLOAT'
+	);
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if (in_array($alter_type, array('ADD', 'DROP'), TRUE))
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' ALTER COLUMN ';
+		$sqls = array();
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			$sqls[] = $sql.$this->_process_column($field[$i]);
+		}
+
+		return $sqls;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		if (isset($attributes['CONSTRAINT']) && strpos($attributes['TYPE'], 'INT') !== FALSE)
+		{
+			unset($attributes['CONSTRAINT']);
+		}
+
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'INTEGER';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'INTEGER':
+				$attributes['TYPE'] = 'INT';
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE)
+		{
+			$field['auto_increment'] = ' IDENTITY(1,1)';
+		}
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_firebird_driver.php b/system/database/drivers/pdo/subdrivers/pdo_firebird_driver.php
new file mode 100644
index 0000000..9778250
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_firebird_driver.php
@@ -0,0 +1,280 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO Firebird Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_firebird_driver extends CI_DB_pdo_driver {
+
+	/**
+	 * Sub-driver
+	 *
+	 * @var	string
+	 */
+	public $subdriver = 'firebird';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('RAND()', 'RAND()');
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Builds the DSN if not already set.
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if (empty($this->dsn))
+		{
+			$this->dsn = 'firebird:';
+
+			if ( ! empty($this->database))
+			{
+				$this->dsn .= 'dbname='.$this->database;
+			}
+			elseif ( ! empty($this->hostname))
+			{
+				$this->dsn .= 'dbname='.$this->hostname;
+			}
+
+			empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set;
+			empty($this->role) OR $this->dsn .= ';role='.$this->role;
+		}
+		elseif ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 9) === FALSE)
+		{
+			$this->dsn .= ';charset='.$this->char_set;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SELECT "RDB$RELATION_NAME" FROM "RDB$RELATIONS" WHERE "RDB$RELATION_NAME" NOT LIKE \'RDB$%\' AND "RDB$RELATION_NAME" NOT LIKE \'MON$%\'';
+
+		if ($prefix_limit === TRUE && $this->dbprefix !== '')
+		{
+			return $sql.' AND "RDB$RELATION_NAME" LIKE \''.$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SELECT "RDB$FIELD_NAME" FROM "RDB$RELATION_FIELDS" WHERE "RDB$RELATION_NAME" = '.$this->escape($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		$sql = 'SELECT "rfields"."RDB$FIELD_NAME" AS "name",
+				CASE "fields"."RDB$FIELD_TYPE"
+					WHEN 7 THEN \'SMALLINT\'
+					WHEN 8 THEN \'INTEGER\'
+					WHEN 9 THEN \'QUAD\'
+					WHEN 10 THEN \'FLOAT\'
+					WHEN 11 THEN \'DFLOAT\'
+					WHEN 12 THEN \'DATE\'
+					WHEN 13 THEN \'TIME\'
+					WHEN 14 THEN \'CHAR\'
+					WHEN 16 THEN \'INT64\'
+					WHEN 27 THEN \'DOUBLE\'
+					WHEN 35 THEN \'TIMESTAMP\'
+					WHEN 37 THEN \'VARCHAR\'
+					WHEN 40 THEN \'CSTRING\'
+					WHEN 261 THEN \'BLOB\'
+					ELSE NULL
+				END AS "type",
+				"fields"."RDB$FIELD_LENGTH" AS "max_length",
+				"rfields"."RDB$DEFAULT_VALUE" AS "default"
+			FROM "RDB$RELATION_FIELDS" "rfields"
+				JOIN "RDB$FIELDS" "fields" ON "rfields"."RDB$FIELD_SOURCE" = "fields"."RDB$FIELD_NAME"
+			WHERE "rfields"."RDB$RELATION_NAME" = '.$this->escape($table).'
+			ORDER BY "rfields"."RDB$FIELD_POSITION"';
+
+		return (($query = $this->query($sql)) !== FALSE)
+			? $query->result_object()
+			: FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update statement
+	 *
+	 * Generates a platform-specific update string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @param	array	$values
+	 * @return	string
+	 */
+	protected function _update($table, $values)
+	{
+		$this->qb_limit = FALSE;
+		return parent::_update($table, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Truncate statement
+	 *
+	 * Generates a platform-specific truncate string from the supplied data
+	 *
+	 * If the database does not support the TRUNCATE statement,
+	 * then this method maps to 'DELETE FROM table'
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _truncate($table)
+	{
+		return 'DELETE FROM '.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete statement
+	 *
+	 * Generates a platform-specific delete string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _delete($table)
+	{
+		$this->qb_limit = FALSE;
+		return parent::_delete($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT
+	 *
+	 * Generates a platform-specific LIMIT clause
+	 *
+	 * @param	string	$sql	SQL Query
+	 * @return	string
+	 */
+	protected function _limit($sql)
+	{
+		// Limit clause depends on if Interbase or Firebird
+		if (stripos($this->version(), 'firebird') !== FALSE)
+		{
+			$select = 'FIRST '.$this->qb_limit
+				.($this->qb_offset > 0 ? ' SKIP '.$this->qb_offset : '');
+		}
+		else
+		{
+			$select = 'ROWS '
+				.($this->qb_offset > 0 ? $this->qb_offset.' TO '.($this->qb_limit + $this->qb_offset) : $this->qb_limit);
+		}
+
+		return preg_replace('`SELECT`i', 'SELECT '.$select, $sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert batch statement
+	 *
+	 * Generates a platform-specific insert string from the supplied data.
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$keys	INSERT keys
+	 * @param	array	$values	INSERT values
+	 * @return	string|bool
+	 */
+	protected function _insert_batch($table, $keys, $values)
+	{
+		return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE;
+	}
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_firebird_forge.php b/system/database/drivers/pdo/subdrivers/pdo_firebird_forge.php
new file mode 100644
index 0000000..26e052a
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_firebird_forge.php
@@ -0,0 +1,238 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO Firebird Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_firebird_forge extends CI_DB_pdo_forge {
+
+	/**
+	 * RENAME TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_rename_table	= FALSE;
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'SMALLINT'	=> 'INTEGER',
+		'INTEGER'	=> 'INT64',
+		'FLOAT'		=> 'DOUBLE PRECISION'
+	);
+
+	/**
+	 * NULL value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_null		= 'NULL';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Create database
+	 *
+	 * @param	string	$db_name
+	 * @return	string
+	 */
+	public function create_database($db_name)
+	{
+		// Firebird databases are flat files, so a path is required
+
+		// Hostname is needed for remote access
+		empty($this->db->hostname) OR $db_name = $this->hostname.':'.$db_name;
+
+		return parent::create_database('"'.$db_name.'"');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Drop database
+	 *
+	 * @param	string	$db_name	(ignored)
+	 * @return	bool
+	 */
+	public function drop_database($db_name)
+	{
+		if ( ! ibase_drop_db($this->conn_id))
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE;
+		}
+		elseif ( ! empty($this->db->data_cache['db_names']))
+		{
+			$key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE);
+			if ($key !== FALSE)
+			{
+				unset($this->db->data_cache['db_names'][$key]);
+			}
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if (in_array($alter_type, array('DROP', 'ADD'), TRUE))
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table);
+		$sqls = array();
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			if ($field[$i]['_literal'] !== FALSE)
+			{
+				return FALSE;
+			}
+
+			if (isset($field[$i]['type']))
+			{
+				$sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+					.' TYPE '.$field[$i]['type'].$field[$i]['length'];
+			}
+
+			if ( ! empty($field[$i]['default']))
+			{
+				$sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+					.' SET '.$field[$i]['default'];
+			}
+
+			if (isset($field[$i]['null']))
+			{
+				$sqls[] = 'UPDATE "RDB$RELATION_FIELDS" SET "RDB$NULL_FLAG" = '
+					.($field[$i]['null'] === TRUE ? 'NULL' : '1')
+					.' WHERE "RDB$FIELD_NAME" = '.$this->db->escape($field[$i]['name'])
+					.' AND "RDB$RELATION_NAME" = '.$this->db->escape($table);
+			}
+
+			if ( ! empty($field[$i]['new_name']))
+			{
+				$sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+					.' TO '.$this->db->escape_identifiers($field[$i]['new_name']);
+			}
+		}
+
+		return $sqls;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process column
+	 *
+	 * @param	array	$field
+	 * @return	string
+	 */
+	protected function _process_column($field)
+	{
+		return $this->db->escape_identifiers($field['name'])
+			.' '.$field['type'].$field['length']
+			.$field['null']
+			.$field['unique']
+			.$field['default'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'TINYINT':
+				$attributes['TYPE'] = 'SMALLINT';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'INTEGER';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'INT':
+				$attributes['TYPE'] = 'INTEGER';
+				return;
+			case 'BIGINT':
+				$attributes['TYPE'] = 'INT64';
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		// Not supported
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_ibm_driver.php b/system/database/drivers/pdo/subdrivers/pdo_ibm_driver.php
new file mode 100644
index 0000000..aca58ec
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_ibm_driver.php
@@ -0,0 +1,245 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO IBM DB2 Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_ibm_driver extends CI_DB_pdo_driver {
+
+	/**
+	 * Sub-driver
+	 *
+	 * @var	string
+	 */
+	public $subdriver = 'ibm';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Builds the DSN if not already set.
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if (empty($this->dsn))
+		{
+			$this->dsn = 'ibm:';
+
+			// Pre-defined DSN
+			if (empty($this->hostname) && empty($this->HOSTNAME) && empty($this->port) && empty($this->PORT))
+			{
+				if (isset($this->DSN))
+				{
+					$this->dsn .= 'DSN='.$this->DSN;
+				}
+				elseif ( ! empty($this->database))
+				{
+					$this->dsn .= 'DSN='.$this->database;
+				}
+
+				return;
+			}
+
+			$this->dsn .= 'DRIVER='.(isset($this->DRIVER) ? '{'.$this->DRIVER.'}' : '{IBM DB2 ODBC DRIVER}').';';
+
+			if (isset($this->DATABASE))
+			{
+				$this->dsn .= 'DATABASE='.$this->DATABASE.';';
+			}
+			elseif ( ! empty($this->database))
+			{
+				$this->dsn .= 'DATABASE='.$this->database.';';
+			}
+
+			if (isset($this->HOSTNAME))
+			{
+				$this->dsn .= 'HOSTNAME='.$this->HOSTNAME.';';
+			}
+			else
+			{
+				$this->dsn .= 'HOSTNAME='.(empty($this->hostname) ? '127.0.0.1;' : $this->hostname.';');
+			}
+
+			if (isset($this->PORT))
+			{
+				$this->dsn .= 'PORT='.$this->port.';';
+			}
+			elseif ( ! empty($this->port))
+			{
+				$this->dsn .= ';PORT='.$this->port.';';
+			}
+
+			$this->dsn .= 'PROTOCOL='.(isset($this->PROTOCOL) ? $this->PROTOCOL.';' : 'TCPIP;');
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SELECT "tabname" FROM "syscat"."tables"
+			WHERE "type" = \'T\' AND LOWER("tabschema") = '.$this->escape(strtolower($this->database));
+
+		if ($prefix_limit === TRUE && $this->dbprefix !== '')
+		{
+			$sql .= ' AND "tabname" LIKE \''.$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SELECT "colname" FROM "syscat"."columns"
+			WHERE LOWER("tabschema") = '.$this->escape(strtolower($this->database)).'
+				AND LOWER("tabname") = '.$this->escape(strtolower($table));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		$sql = 'SELECT "colname" AS "name", "typename" AS "type", "default" AS "default", "length" AS "max_length",
+				CASE "keyseq" WHEN NULL THEN 0 ELSE 1 END AS "primary_key"
+			FROM "syscat"."columns"
+			WHERE LOWER("tabschema") = '.$this->escape(strtolower($this->database)).'
+				AND LOWER("tabname") = '.$this->escape(strtolower($table)).'
+			ORDER BY "colno"';
+
+		return (($query = $this->query($sql)) !== FALSE)
+			? $query->result_object()
+			: FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update statement
+	 *
+	 * Generates a platform-specific update string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @param	array	$values
+	 * @return	string
+	 */
+	protected function _update($table, $values)
+	{
+		$this->qb_limit = FALSE;
+		$this->qb_orderby = array();
+		return parent::_update($table, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete statement
+	 *
+	 * Generates a platform-specific delete string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _delete($table)
+	{
+		$this->qb_limit = FALSE;
+		return parent::_delete($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT
+	 *
+	 * Generates a platform-specific LIMIT clause
+	 *
+	 * @param	string	$sql	SQL Query
+	 * @return	string
+	 */
+	protected function _limit($sql)
+	{
+		$sql .= ' FETCH FIRST '.($this->qb_limit + $this->qb_offset).' ROWS ONLY';
+
+		return ($this->qb_offset)
+			? 'SELECT * FROM ('.$sql.') WHERE rownum > '.$this->qb_offset
+			: $sql;
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_ibm_forge.php b/system/database/drivers/pdo/subdrivers/pdo_ibm_forge.php
new file mode 100644
index 0000000..cf023d4
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_ibm_forge.php
@@ -0,0 +1,155 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO IBM DB2 Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_ibm_forge extends CI_DB_pdo_forge {
+
+	/**
+	 * RENAME TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_rename_table	= 'RENAME TABLE %s TO %s';
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'SMALLINT'	=> 'INTEGER',
+		'INT'		=> 'BIGINT',
+		'INTEGER'	=> 'BIGINT'
+	);
+
+	/**
+	 * DEFAULT value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_default		= FALSE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if ($alter_type === 'CHANGE')
+		{
+			$alter_type = 'MODIFY';
+		}
+
+		return parent::_alter_table($alter_type, $table, $field);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'TINYINT':
+				$attributes['TYPE'] = 'SMALLINT';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'INTEGER';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute UNIQUE
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_unique(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === TRUE)
+		{
+			$field['unique'] = ' UNIQUE';
+
+			// UNIQUE must be used with NOT NULL
+			$field['null'] = ' NOT NULL';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		// Not supported
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_informix_driver.php b/system/database/drivers/pdo/subdrivers/pdo_informix_driver.php
new file mode 100644
index 0000000..4d230c3
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_informix_driver.php
@@ -0,0 +1,310 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO Informix Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_informix_driver extends CI_DB_pdo_driver {
+
+	/**
+	 * Sub-driver
+	 *
+	 * @var	string
+	 */
+	public $subdriver = 'informix';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('ASC', 'ASC'); // Currently not supported
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Builds the DSN if not already set.
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if (empty($this->dsn))
+		{
+			$this->dsn = 'informix:';
+
+			// Pre-defined DSN
+			if (empty($this->hostname) && empty($this->host) && empty($this->port) && empty($this->service))
+			{
+				if (isset($this->DSN))
+				{
+					$this->dsn .= 'DSN='.$this->DSN;
+				}
+				elseif ( ! empty($this->database))
+				{
+					$this->dsn .= 'DSN='.$this->database;
+				}
+
+				return;
+			}
+
+			if (isset($this->host))
+			{
+				$this->dsn .= 'host='.$this->host;
+			}
+			else
+			{
+				$this->dsn .= 'host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname);
+			}
+
+			if (isset($this->service))
+			{
+				$this->dsn .= '; service='.$this->service;
+			}
+			elseif ( ! empty($this->port))
+			{
+				$this->dsn .= '; service='.$this->port;
+			}
+
+			empty($this->database) OR $this->dsn .= '; database='.$this->database;
+			empty($this->server) OR $this->dsn .= '; server='.$this->server;
+
+			$this->dsn .= '; protocol='.(isset($this->protocol) ? $this->protocol : 'onsoctcp')
+				.'; EnableScrollableCursors=1';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SELECT "tabname" FROM "systables"
+			WHERE "tabid" > 99 AND "tabtype" = \'T\' AND LOWER("owner") = '.$this->escape(strtolower($this->username));
+
+		if ($prefix_limit === TRUE && $this->dbprefix !== '')
+		{
+			$sql .= ' AND "tabname" LIKE \''.$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		if (strpos($table, '.') !== FALSE)
+		{
+			sscanf($table, '%[^.].%s', $owner, $table);
+		}
+		else
+		{
+			$owner = $this->username;
+		}
+
+		return 'SELECT "colname" FROM "systables", "syscolumns"
+			WHERE "systables"."tabid" = "syscolumns"."tabid"
+				AND "systables"."tabtype" = \'T\'
+				AND LOWER("systables"."owner") = '.$this->escape(strtolower($owner)).'
+				AND LOWER("systables"."tabname") = '.$this->escape(strtolower($table));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		$sql = 'SELECT "syscolumns"."colname" AS "name",
+				CASE "syscolumns"."coltype"
+					WHEN 0 THEN \'CHAR\'
+					WHEN 1 THEN \'SMALLINT\'
+					WHEN 2 THEN \'INTEGER\'
+					WHEN 3 THEN \'FLOAT\'
+					WHEN 4 THEN \'SMALLFLOAT\'
+					WHEN 5 THEN \'DECIMAL\'
+					WHEN 6 THEN \'SERIAL\'
+					WHEN 7 THEN \'DATE\'
+					WHEN 8 THEN \'MONEY\'
+					WHEN 9 THEN \'NULL\'
+					WHEN 10 THEN \'DATETIME\'
+					WHEN 11 THEN \'BYTE\'
+					WHEN 12 THEN \'TEXT\'
+					WHEN 13 THEN \'VARCHAR\'
+					WHEN 14 THEN \'INTERVAL\'
+					WHEN 15 THEN \'NCHAR\'
+					WHEN 16 THEN \'NVARCHAR\'
+					WHEN 17 THEN \'INT8\'
+					WHEN 18 THEN \'SERIAL8\'
+					WHEN 19 THEN \'SET\'
+					WHEN 20 THEN \'MULTISET\'
+					WHEN 21 THEN \'LIST\'
+					WHEN 22 THEN \'Unnamed ROW\'
+					WHEN 40 THEN \'LVARCHAR\'
+					WHEN 41 THEN \'BLOB/CLOB/BOOLEAN\'
+					WHEN 4118 THEN \'Named ROW\'
+					ELSE "syscolumns"."coltype"
+				END AS "type",
+				"syscolumns"."collength" as "max_length",
+				CASE "sysdefaults"."type"
+					WHEN \'L\' THEN "sysdefaults"."default"
+					ELSE NULL
+				END AS "default"
+			FROM "syscolumns", "systables", "sysdefaults"
+			WHERE "syscolumns"."tabid" = "systables"."tabid"
+				AND "systables"."tabid" = "sysdefaults"."tabid"
+				AND "syscolumns"."colno" = "sysdefaults"."colno"
+				AND "systables"."tabtype" = \'T\'
+				AND LOWER("systables"."owner") = '.$this->escape(strtolower($this->username)).'
+				AND LOWER("systables"."tabname") = '.$this->escape(strtolower($table)).'
+			ORDER BY "syscolumns"."colno"';
+
+		return (($query = $this->query($sql)) !== FALSE)
+			? $query->result_object()
+			: FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update statement
+	 *
+	 * Generates a platform-specific update string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @param	array	$values
+	 * @return	string
+	 */
+	protected function _update($table, $values)
+	{
+		$this->qb_limit = FALSE;
+		$this->qb_orderby = array();
+		return parent::_update($table, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Truncate statement
+	 *
+	 * Generates a platform-specific truncate string from the supplied data
+	 *
+	 * If the database does not support the TRUNCATE statement,
+	 * then this method maps to 'DELETE FROM table'
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _truncate($table)
+	{
+		return 'TRUNCATE TABLE ONLY '.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete statement
+	 *
+	 * Generates a platform-specific delete string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _delete($table)
+	{
+		$this->qb_limit = FALSE;
+		return parent::_delete($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT
+	 *
+	 * Generates a platform-specific LIMIT clause
+	 *
+	 * @param	string	$sql	$SQL Query
+	 * @return	string
+	 */
+	protected function _limit($sql)
+	{
+		$select = 'SELECT '.($this->qb_offset ? 'SKIP '.$this->qb_offset : '').'FIRST '.$this->qb_limit.' ';
+		return preg_replace('/^(SELECT\s)/i', $select, $sql, 1);
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_informix_forge.php b/system/database/drivers/pdo/subdrivers/pdo_informix_forge.php
new file mode 100644
index 0000000..368d8dc
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_informix_forge.php
@@ -0,0 +1,164 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO Informix Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_informix_forge extends CI_DB_pdo_forge {
+
+	/**
+	 * RENAME TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_rename_table	= 'RENAME TABLE %s TO %s';
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'SMALLINT'	=> 'INTEGER',
+		'INT'		=> 'BIGINT',
+		'INTEGER'	=> 'BIGINT',
+		'REAL'		=> 'DOUBLE PRECISION',
+		'SMALLFLOAT'	=> 'DOUBLE PRECISION'
+	);
+
+	/**
+	 * DEFAULT value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_default		= ', ';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if ($alter_type === 'CHANGE')
+		{
+			$alter_type = 'MODIFY';
+		}
+
+		return parent::_alter_table($alter_type, $table, $field);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'TINYINT':
+				$attributes['TYPE'] = 'SMALLINT';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'INTEGER';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'BYTE':
+			case 'TEXT':
+			case 'BLOB':
+			case 'CLOB':
+				$attributes['UNIQUE'] = FALSE;
+				if (isset($attributes['DEFAULT']))
+				{
+					unset($attributes['DEFAULT']);
+				}
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute UNIQUE
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_unique(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === TRUE)
+		{
+			$field['unique'] = ' UNIQUE CONSTRAINT '.$this->db->escape_identifiers($field['name']);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		// Not supported
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_mysql_driver.php b/system/database/drivers/pdo/subdrivers/pdo_mysql_driver.php
new file mode 100644
index 0000000..1ad854d
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_mysql_driver.php
@@ -0,0 +1,380 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO MySQL Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_mysql_driver extends CI_DB_pdo_driver {
+
+	/**
+	 * Sub-driver
+	 *
+	 * @var	string
+	 */
+	public $subdriver = 'mysql';
+
+	/**
+	 * Compression flag
+	 *
+	 * @var	bool
+	 */
+	public $compress = FALSE;
+
+	/**
+	 * Strict ON flag
+	 *
+	 * Whether we're running in strict SQL mode.
+	 *
+	 * @var	bool
+	 */
+	public $stricton;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Identifier escape character
+	 *
+	 * @var	string
+	 */
+	protected $_escape_char = '`';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Builds the DSN if not already set.
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if (empty($this->dsn))
+		{
+			$this->dsn = 'mysql:host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname);
+
+			empty($this->port) OR $this->dsn .= ';port='.$this->port;
+			empty($this->database) OR $this->dsn .= ';dbname='.$this->database;
+			empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set;
+		}
+		elseif ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 6) === FALSE)
+		{
+			$this->dsn .= ';charset='.$this->char_set;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	object
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		if (isset($this->stricton))
+		{
+			if ($this->stricton)
+			{
+				$sql = 'CONCAT(@@sql_mode, ",", "STRICT_ALL_TABLES")';
+			}
+			else
+			{
+				$sql = 'REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
+                                        @@sql_mode,
+                                        "STRICT_ALL_TABLES,", ""),
+                                        ",STRICT_ALL_TABLES", ""),
+                                        "STRICT_ALL_TABLES", ""),
+                                        "STRICT_TRANS_TABLES,", ""),
+                                        ",STRICT_TRANS_TABLES", ""),
+                                        "STRICT_TRANS_TABLES", "")';
+			}
+
+			if ( ! empty($sql))
+			{
+				if (empty($this->options[PDO::MYSQL_ATTR_INIT_COMMAND]))
+				{
+					$this->options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET SESSION sql_mode = '.$sql;
+				}
+				else
+				{
+					$this->options[PDO::MYSQL_ATTR_INIT_COMMAND] .= ', @@session.sql_mode = '.$sql;
+				}
+			}
+		}
+
+		if ($this->compress === TRUE)
+		{
+			$this->options[PDO::MYSQL_ATTR_COMPRESS] = TRUE;
+		}
+
+		if (is_array($this->encrypt))
+		{
+			$ssl = array();
+			empty($this->encrypt['ssl_key'])    OR $ssl[PDO::MYSQL_ATTR_SSL_KEY]    = $this->encrypt['ssl_key'];
+			empty($this->encrypt['ssl_cert'])   OR $ssl[PDO::MYSQL_ATTR_SSL_CERT]   = $this->encrypt['ssl_cert'];
+			empty($this->encrypt['ssl_ca'])     OR $ssl[PDO::MYSQL_ATTR_SSL_CA]     = $this->encrypt['ssl_ca'];
+			empty($this->encrypt['ssl_capath']) OR $ssl[PDO::MYSQL_ATTR_SSL_CAPATH] = $this->encrypt['ssl_capath'];
+			empty($this->encrypt['ssl_cipher']) OR $ssl[PDO::MYSQL_ATTR_SSL_CIPHER] = $this->encrypt['ssl_cipher'];
+
+			if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') && isset($this->encrypt['ssl_verify']))
+			{
+				$ssl[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $this->encrypt['ssl_verify'];
+			}
+
+			// DO NOT use array_merge() here!
+			// It re-indexes numeric keys and the PDO_MYSQL_ATTR_SSL_* constants are integers.
+			empty($ssl) OR $this->options += $ssl;
+		}
+
+		// Prior to version 5.7.3, MySQL silently downgrades to an unencrypted connection if SSL setup fails
+		if (
+			($pdo = parent::db_connect($persistent)) !== FALSE
+			&& ! empty($ssl)
+			&& version_compare($pdo->getAttribute(PDO::ATTR_CLIENT_VERSION), '5.7.3', '<=')
+			&& empty($pdo->query("SHOW STATUS LIKE 'ssl_cipher'")->fetchObject()->Value)
+		)
+		{
+			$message = 'PDO_MYSQL was configured for an SSL connection, but got an unencrypted connection instead!';
+			log_message('error', $message);
+			return ($this->db_debug) ? $this->display_error($message, '', TRUE) : FALSE;
+		}
+
+		return $pdo;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Select the database
+	 *
+	 * @param	string	$database
+	 * @return	bool
+	 */
+	public function db_select($database = '')
+	{
+		if ($database === '')
+		{
+			$database = $this->database;
+		}
+
+		if (FALSE !== $this->simple_query('USE '.$this->escape_identifiers($database)))
+		{
+			$this->database = $database;
+			$this->data_cache = array();
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Begin Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_begin()
+	{
+		$this->conn_id->setAttribute(PDO::ATTR_AUTOCOMMIT, FALSE);
+		return $this->conn_id->beginTransaction();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Commit Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_commit()
+	{
+		if ($this->conn_id->commit())
+		{
+			$this->conn_id->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE);
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rollback Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_rollback()
+	{
+		if ($this->conn_id->rollBack())
+		{
+			$this->conn_id->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE);
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SHOW TABLES FROM '.$this->_escape_char.$this->database.$this->_escape_char;
+
+		if ($prefix_limit === TRUE && $this->dbprefix !== '')
+		{
+			return $sql." LIKE '".$this->escape_like_str($this->dbprefix)."%'";
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		if (($query = $this->query('SHOW COLUMNS FROM '.$this->protect_identifiers($table, TRUE, NULL, FALSE))) === FALSE)
+		{
+			return FALSE;
+		}
+		$query = $query->result_object();
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]->Field;
+
+			sscanf($query[$i]->Type, '%[a-z](%d)',
+				$retval[$i]->type,
+				$retval[$i]->max_length
+			);
+
+			$retval[$i]->default		= $query[$i]->Default;
+			$retval[$i]->primary_key	= (int) ($query[$i]->Key === 'PRI');
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Truncate statement
+	 *
+	 * Generates a platform-specific truncate string from the supplied data
+	 *
+	 * If the database does not support the TRUNCATE statement,
+	 * then this method maps to 'DELETE FROM table'
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _truncate($table)
+	{
+		return 'TRUNCATE '.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * FROM tables
+	 *
+	 * Groups tables in FROM clauses if needed, so there is no confusion
+	 * about operator precedence.
+	 *
+	 * @return	string
+	 */
+	protected function _from_tables()
+	{
+		if ( ! empty($this->qb_join) && count($this->qb_from) > 1)
+		{
+			return '('.implode(', ', $this->qb_from).')';
+		}
+
+		return implode(', ', $this->qb_from);
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_mysql_forge.php b/system/database/drivers/pdo/subdrivers/pdo_mysql_forge.php
new file mode 100644
index 0000000..8bf5cfb
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_mysql_forge.php
@@ -0,0 +1,257 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO MySQL Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_mysql_forge extends CI_DB_pdo_forge {
+
+	/**
+	 * CREATE DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_database	= 'CREATE DATABASE %s CHARACTER SET %s COLLATE %s';
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= 'CREATE TABLE IF NOT EXISTS';
+
+	/**
+	 * CREATE TABLE keys flag
+	 *
+	 * Whether table keys are created from within the
+	 * CREATE TABLE statement.
+	 *
+	 * @var	bool
+	 */
+	protected $_create_table_keys	= TRUE;
+
+	/**
+	 * DROP TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_table_if	= 'DROP TABLE IF EXISTS';
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'TINYINT',
+		'SMALLINT',
+		'MEDIUMINT',
+		'INT',
+		'INTEGER',
+		'BIGINT',
+		'REAL',
+		'DOUBLE',
+		'DOUBLE PRECISION',
+		'FLOAT',
+		'DECIMAL',
+		'NUMERIC'
+	);
+
+	/**
+	 * NULL value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_null = 'NULL';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * CREATE TABLE attributes
+	 *
+	 * @param	array	$attributes	Associative array of table attributes
+	 * @return	string
+	 */
+	protected function _create_table_attr($attributes)
+	{
+		$sql = '';
+
+		foreach (array_keys($attributes) as $key)
+		{
+			if (is_string($key))
+			{
+				$sql .= ' '.strtoupper($key).' = '.$attributes[$key];
+			}
+		}
+
+		if ( ! empty($this->db->char_set) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET'))
+		{
+			$sql .= ' DEFAULT CHARACTER SET = '.$this->db->char_set;
+		}
+
+		if ( ! empty($this->db->dbcollat) && ! strpos($sql, 'COLLATE'))
+		{
+			$sql .= ' COLLATE = '.$this->db->dbcollat;
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if ($alter_type === 'DROP')
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table);
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			if ($field[$i]['_literal'] !== FALSE)
+			{
+				$field[$i] = ($alter_type === 'ADD')
+						? "\n\tADD ".$field[$i]['_literal']
+						: "\n\tMODIFY ".$field[$i]['_literal'];
+			}
+			else
+			{
+				if ($alter_type === 'ADD')
+				{
+					$field[$i]['_literal'] = "\n\tADD ";
+				}
+				else
+				{
+					$field[$i]['_literal'] = empty($field[$i]['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE ";
+				}
+
+				$field[$i] = $field[$i]['_literal'].$this->_process_column($field[$i]);
+			}
+		}
+
+		return array($sql.implode(',', $field));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process column
+	 *
+	 * @param	array	$field
+	 * @return	string
+	 */
+	protected function _process_column($field)
+	{
+		$extra_clause = isset($field['after'])
+			? ' AFTER '.$this->db->escape_identifiers($field['after']) : '';
+
+		if (empty($extra_clause) && isset($field['first']) && $field['first'] === TRUE)
+		{
+			$extra_clause = ' FIRST';
+		}
+
+		return $this->db->escape_identifiers($field['name'])
+			.(empty($field['new_name']) ? '' : ' '.$this->db->escape_identifiers($field['new_name']))
+			.' '.$field['type'].$field['length']
+			.$field['unsigned']
+			.$field['null']
+			.$field['default']
+			.$field['auto_increment']
+			.$field['unique']
+			.(empty($field['comment']) ? '' : ' COMMENT '.$field['comment'])
+			.$extra_clause;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process indexes
+	 *
+	 * @param	string	$table	(ignored)
+	 * @return	string
+	 */
+	protected function _process_indexes($table)
+	{
+		$sql = '';
+
+		for ($i = 0, $c = count($this->keys); $i < $c; $i++)
+		{
+			if (is_array($this->keys[$i]))
+			{
+				for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++)
+				{
+					if ( ! isset($this->fields[$this->keys[$i][$i2]]))
+					{
+						unset($this->keys[$i][$i2]);
+						continue;
+					}
+				}
+			}
+			elseif ( ! isset($this->fields[$this->keys[$i]]))
+			{
+				unset($this->keys[$i]);
+				continue;
+			}
+
+			is_array($this->keys[$i]) OR $this->keys[$i] = array($this->keys[$i]);
+
+			$sql .= ",\n\tKEY ".$this->db->escape_identifiers(implode('_', $this->keys[$i]))
+				.' ('.implode(', ', $this->db->escape_identifiers($this->keys[$i])).')';
+		}
+
+		$this->keys = array();
+
+		return $sql;
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_oci_driver.php b/system/database/drivers/pdo/subdrivers/pdo_oci_driver.php
new file mode 100644
index 0000000..3573691
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_oci_driver.php
@@ -0,0 +1,327 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO Oracle Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_oci_driver extends CI_DB_pdo_driver {
+
+	/**
+	 * Sub-driver
+	 *
+	 * @var	string
+	 */
+	public $subdriver = 'oci';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * List of reserved identifiers
+	 *
+	 * Identifiers that must NOT be escaped.
+	 *
+	 * @var	string[]
+	 */
+	protected $_reserved_identifiers = array('*', 'rownum');
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('ASC', 'ASC'); // Currently not supported
+
+	/**
+	 * COUNT string
+	 *
+	 * @used-by	CI_DB_driver::count_all()
+	 * @used-by	CI_DB_query_builder::count_all_results()
+	 *
+	 * @var	string
+	 */
+	protected $_count_string = 'SELECT COUNT(1) AS ';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Builds the DSN if not already set.
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if (empty($this->dsn))
+		{
+			$this->dsn = 'oci:dbname=';
+
+			// Oracle has a slightly different PDO DSN format (Easy Connect),
+			// which also supports pre-defined DSNs.
+			if (empty($this->hostname) && empty($this->port))
+			{
+				$this->dsn .= $this->database;
+			}
+			else
+			{
+				$this->dsn .= '//'.(empty($this->hostname) ? '127.0.0.1' : $this->hostname)
+					.(empty($this->port) ? '' : ':'.$this->port).'/';
+
+				empty($this->database) OR $this->dsn .= $this->database;
+			}
+
+			empty($this->char_set) OR $this->dsn .= ';charset='.$this->char_set;
+		}
+		elseif ( ! empty($this->char_set) && strpos($this->dsn, 'charset=', 4) === FALSE)
+		{
+			$this->dsn .= ';charset='.$this->char_set;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database version number
+	 *
+	 * @return	string
+	 */
+	public function version()
+	{
+		if (isset($this->data_cache['version']))
+		{
+			return $this->data_cache['version'];
+		}
+
+		$version_string = parent::version();
+		if (preg_match('#(Release\s)?(?<version>\d+(?:\.\d+)+)#', $version_string, $match))
+		{
+			return $this->data_cache['version'] = $match['version'];
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SELECT "TABLE_NAME" FROM "ALL_TABLES"';
+
+		if ($prefix_limit === TRUE && $this->dbprefix !== '')
+		{
+			return $sql.' WHERE "TABLE_NAME" LIKE \''.$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		if (strpos($table, '.') !== FALSE)
+		{
+			sscanf($table, '%[^.].%s', $owner, $table);
+		}
+		else
+		{
+			$owner = $this->username;
+		}
+
+		return 'SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS
+			WHERE UPPER(OWNER) = '.$this->escape(strtoupper($owner)).'
+				AND UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		if (strpos($table, '.') !== FALSE)
+		{
+			sscanf($table, '%[^.].%s', $owner, $table);
+		}
+		else
+		{
+			$owner = $this->username;
+		}
+
+		$sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHAR_LENGTH, DATA_PRECISION, DATA_LENGTH, DATA_DEFAULT, NULLABLE
+			FROM ALL_TAB_COLUMNS
+			WHERE UPPER(OWNER) = '.$this->escape(strtoupper($owner)).'
+				AND UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
+
+		if (($query = $this->query($sql)) === FALSE)
+		{
+			return FALSE;
+		}
+		$query = $query->result_object();
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]->COLUMN_NAME;
+			$retval[$i]->type		= $query[$i]->DATA_TYPE;
+
+			$length = ($query[$i]->CHAR_LENGTH > 0)
+				? $query[$i]->CHAR_LENGTH : $query[$i]->DATA_PRECISION;
+			if ($length === NULL)
+			{
+				$length = $query[$i]->DATA_LENGTH;
+			}
+			$retval[$i]->max_length		= $length;
+
+			$default = $query[$i]->DATA_DEFAULT;
+			if ($default === NULL && $query[$i]->NULLABLE === 'N')
+			{
+				$default = '';
+			}
+			$retval[$i]->default		= $query[$i]->COLUMN_DEFAULT;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert batch statement
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$keys	INSERT keys
+	 * @param	array	$values	INSERT values
+	 * @return 	string
+	 */
+	protected function _insert_batch($table, $keys, $values)
+	{
+		$keys = implode(', ', $keys);
+		$sql = "INSERT ALL\n";
+
+		for ($i = 0, $c = count($values); $i < $c; $i++)
+		{
+			$sql .= '	INTO '.$table.' ('.$keys.') VALUES '.$values[$i]."\n";
+		}
+
+		return $sql.'SELECT * FROM dual';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete statement
+	 *
+	 * Generates a platform-specific delete string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _delete($table)
+	{
+		if ($this->qb_limit)
+		{
+			$this->where('rownum <= ',$this->qb_limit, FALSE);
+			$this->qb_limit = FALSE;
+		}
+
+		return parent::_delete($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT
+	 *
+	 * Generates a platform-specific LIMIT clause
+	 *
+	 * @param	string	$sql	SQL Query
+	 * @return	string
+	 */
+	protected function _limit($sql)
+	{
+		if (version_compare($this->version(), '12.1', '>='))
+		{
+			// OFFSET-FETCH can be used only with the ORDER BY clause
+			empty($this->qb_orderby) && $sql .= ' ORDER BY 1';
+
+			return $sql.' OFFSET '.(int) $this->qb_offset.' ROWS FETCH NEXT '.$this->qb_limit.' ROWS ONLY';
+		}
+
+		return 'SELECT * FROM (SELECT inner_query.*, rownum rnum FROM ('.$sql.') inner_query WHERE rownum < '.($this->qb_offset + $this->qb_limit + 1).')'
+			.($this->qb_offset ? ' WHERE rnum >= '.($this->qb_offset + 1): '');
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_oci_forge.php b/system/database/drivers/pdo/subdrivers/pdo_oci_forge.php
new file mode 100644
index 0000000..0783cd5
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_oci_forge.php
@@ -0,0 +1,208 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO Oracle Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_oci_forge extends CI_DB_pdo_forge {
+
+	/**
+	 * CREATE DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_database	= FALSE;
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= FALSE;
+
+	/**
+	 * DROP DATABASE statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_database	= FALSE;
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	bool|array
+	 */
+	protected $_unsigned		= FALSE;
+
+	/**
+	 * NULL value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_null		= 'NULL';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if ($alter_type === 'DROP')
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+		elseif ($alter_type === 'CHANGE')
+		{
+			$alter_type = 'MODIFY';
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table);
+		$sqls = array();
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			if ($field[$i]['_literal'] !== FALSE)
+			{
+				$field[$i] = "\n\t".$field[$i]['_literal'];
+			}
+			else
+			{
+				$field[$i]['_literal'] = "\n\t".$this->_process_column($field[$i]);
+
+				if ( ! empty($field[$i]['comment']))
+				{
+					$sqls[] = 'COMMENT ON COLUMN '
+						.$this->db->escape_identifiers($table).'.'.$this->db->escape_identifiers($field[$i]['name'])
+						.' IS '.$field[$i]['comment'];
+				}
+
+				if ($alter_type === 'MODIFY' && ! empty($field[$i]['new_name']))
+				{
+					$sqls[] = $sql.' RENAME COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+						.' TO '.$this->db->escape_identifiers($field[$i]['new_name']);
+				}
+			}
+		}
+
+		$sql .= ' '.$alter_type.' ';
+		$sql .= (count($field) === 1)
+				? $field[0]
+				: '('.implode(',', $field).')';
+
+		// RENAME COLUMN must be executed after MODIFY
+		array_unshift($sqls, $sql);
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'number') !== FALSE && version_compare($this->db->version(), '12.1', '>='))
+		{
+			$field['auto_increment'] = ' GENERATED ALWAYS AS IDENTITY';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process column
+	 *
+	 * @param	array	$field
+	 * @return	string
+	 */
+	protected function _process_column($field)
+	{
+		return $this->db->escape_identifiers($field['name'])
+			.' '.$field['type'].$field['length']
+			.$field['unsigned']
+			.$field['default']
+			.$field['auto_increment']
+			.$field['null']
+			.$field['unique'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'TINYINT':
+				$attributes['TYPE'] = 'NUMBER';
+				return;
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'NUMBER';
+				return;
+			case 'INT':
+				$attributes['TYPE'] = 'NUMBER';
+				return;
+			case 'BIGINT':
+				$attributes['TYPE'] = 'NUMBER';
+				return;
+			default: return;
+		}
+	}
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_odbc_driver.php b/system/database/drivers/pdo/subdrivers/pdo_odbc_driver.php
new file mode 100644
index 0000000..6b7f237
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_odbc_driver.php
@@ -0,0 +1,230 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO ODBC Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_odbc_driver extends CI_DB_pdo_driver {
+
+	/**
+	 * Sub-driver
+	 *
+	 * @var	string
+	 */
+	public $subdriver = 'odbc';
+
+	/**
+	 * Database schema
+	 *
+	 * @var	string
+	 */
+	public $schema = 'public';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Identifier escape character
+	 *
+	 * Must be empty for ODBC.
+	 *
+	 * @var	string
+	 */
+	protected $_escape_char = '';
+
+	/**
+	 * ESCAPE statement string
+	 *
+	 * @var	string
+	 */
+	protected $_like_escape_str = " {escape '%s'} ";
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('RND()', 'RND(%d)');
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Builds the DSN if not already set.
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if (empty($this->dsn))
+		{
+			$this->dsn = 'odbc:';
+
+			// Pre-defined DSN
+			if (empty($this->hostname) && empty($this->HOSTNAME) && empty($this->port) && empty($this->PORT))
+			{
+				if (isset($this->DSN))
+				{
+					$this->dsn .= 'DSN='.$this->DSN;
+				}
+				elseif ( ! empty($this->database))
+				{
+					$this->dsn .= 'DSN='.$this->database;
+				}
+
+				return;
+			}
+
+			// If the DSN is not pre-configured - try to build an IBM DB2 connection string
+			$this->dsn .= 'DRIVER='.(isset($this->DRIVER) ? '{'.$this->DRIVER.'}' : '{IBM DB2 ODBC DRIVER}').';';
+
+			if (isset($this->DATABASE))
+			{
+				$this->dsn .= 'DATABASE='.$this->DATABASE.';';
+			}
+			elseif ( ! empty($this->database))
+			{
+				$this->dsn .= 'DATABASE='.$this->database.';';
+			}
+
+			if (isset($this->HOSTNAME))
+			{
+				$this->dsn .= 'HOSTNAME='.$this->HOSTNAME.';';
+			}
+			else
+			{
+				$this->dsn .= 'HOSTNAME='.(empty($this->hostname) ? '127.0.0.1;' : $this->hostname.';');
+			}
+
+			if (isset($this->PORT))
+			{
+				$this->dsn .= 'PORT='.$this->port.';';
+			}
+			elseif ( ! empty($this->port))
+			{
+				$this->dsn .= ';PORT='.$this->port.';';
+			}
+
+			$this->dsn .= 'PROTOCOL='.(isset($this->PROTOCOL) ? $this->PROTOCOL.';' : 'TCPIP;');
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Platform-dependent string escape
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _escape_str($str)
+	{
+		$this->display_error('db_unsupported_feature');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Determines if a query is a "write" type.
+	 *
+	 * @param	string	An SQL query string
+	 * @return	bool
+	 */
+	public function is_write_type($sql)
+	{
+		if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql))
+		{
+			return FALSE;
+		}
+
+		return parent::is_write_type($sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = '".$this->schema."'";
+
+		if ($prefix_limit !== FALSE && $this->dbprefix !== '')
+		{
+			return $sql." AND table_name LIKE '".$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SELECT column_name FROM information_schema.columns WHERE table_name = '.$this->escape($table);
+	}
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_odbc_forge.php b/system/database/drivers/pdo/subdrivers/pdo_odbc_forge.php
new file mode 100644
index 0000000..c9b8238
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_odbc_forge.php
@@ -0,0 +1,71 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO ODBC Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/database/
+ */
+class CI_DB_pdo_odbc_forge extends CI_DB_pdo_forge {
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	bool|array
+	 */
+	protected $_unsigned		= FALSE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		// Not supported (in most databases at least)
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_pgsql_driver.php b/system/database/drivers/pdo/subdrivers/pdo_pgsql_driver.php
new file mode 100644
index 0000000..297cc6f
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_pgsql_driver.php
@@ -0,0 +1,385 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO PostgreSQL Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_pgsql_driver extends CI_DB_pdo_driver {
+
+	/**
+	 * Sub-driver
+	 *
+	 * @var	string
+	 */
+	public $subdriver = 'pgsql';
+
+	/**
+	 * Database schema
+	 *
+	 * @var	string
+	 */
+	public $schema = 'public';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('RANDOM()', 'RANDOM()');
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Builds the DSN if not already set.
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if (empty($this->dsn))
+		{
+			$this->dsn = 'pgsql:host='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname);
+
+			empty($this->port) OR $this->dsn .= ';port='.$this->port;
+			empty($this->database) OR $this->dsn .= ';dbname='.$this->database;
+
+			if ( ! empty($this->username))
+			{
+				$this->dsn .= ';user='.$this->username;
+				empty($this->password) OR $this->dsn .= ';password='.$this->password;
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	object
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		$this->conn_id = parent::db_connect($persistent);
+
+		if (is_object($this->conn_id) && ! empty($this->schema))
+		{
+			$this->simple_query('SET search_path TO '.$this->schema.',public');
+		}
+
+		return $this->conn_id;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert ID
+	 *
+	 * @param	string	$name
+	 * @return	int
+	 */
+	public function insert_id($name = NULL)
+	{
+		if ($name === NULL && version_compare($this->version(), '8.1', '>='))
+		{
+			$query = $this->query('SELECT LASTVAL() AS ins_id');
+			$query = $query->row();
+			return $query->ins_id;
+		}
+
+		return $this->conn_id->lastInsertId($name);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Determines if a query is a "write" type.
+	 *
+	 * @param	string	An SQL query string
+	 * @return	bool
+	 */
+	public function is_write_type($sql)
+	{
+		if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql))
+		{
+			return FALSE;
+		}
+
+		return parent::is_write_type($sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * "Smart" Escape String
+	 *
+	 * Escapes data based on type
+	 *
+	 * @param	string	$str
+	 * @return	mixed
+	 */
+	public function escape($str)
+	{
+		if (is_bool($str))
+		{
+			return ($str) ? 'TRUE' : 'FALSE';
+		}
+
+		return parent::escape($str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY
+	 *
+	 * @param	string	$orderby
+	 * @param	string	$direction	ASC, DESC or RANDOM
+	 * @param	bool	$escape
+	 * @return	object
+	 */
+	public function order_by($orderby, $direction = '', $escape = NULL)
+	{
+		$direction = strtoupper(trim($direction));
+		if ($direction === 'RANDOM')
+		{
+			if ( ! is_float($orderby) && ctype_digit((string) $orderby))
+			{
+				$orderby = ($orderby > 1)
+					? (float) '0.'.$orderby
+					: (float) $orderby;
+			}
+
+			if (is_float($orderby))
+			{
+				$this->simple_query('SET SEED '.$orderby);
+			}
+
+			$orderby = $this->_random_keyword[0];
+			$direction = '';
+			$escape = FALSE;
+		}
+
+		return parent::order_by($orderby, $direction, $escape);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \''.$this->schema."'";
+
+		if ($prefix_limit === TRUE && $this->dbprefix !== '')
+		{
+			return $sql.' AND "table_name" LIKE \''
+				.$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * List column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SELECT "column_name"
+			FROM "information_schema"."columns"
+			WHERE "table_schema" = \''.$this->schema.'\' AND LOWER("table_name") = '.$this->escape(strtolower($table));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		$sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default"
+			FROM "information_schema"."columns"
+			WHERE "table_schema" = \''.$this->schema.'\' AND LOWER("table_name") = '.$this->escape(strtolower($table));
+
+		if (($query = $this->query($sql)) === FALSE)
+		{
+			return FALSE;
+		}
+		$query = $query->result_object();
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]->column_name;
+			$retval[$i]->type		= $query[$i]->data_type;
+			$retval[$i]->max_length		= ($query[$i]->character_maximum_length > 0) ? $query[$i]->character_maximum_length : $query[$i]->numeric_precision;
+			$retval[$i]->default		= $query[$i]->column_default;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update statement
+	 *
+	 * Generates a platform-specific update string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @param	array	$values
+	 * @return	string
+	 */
+	protected function _update($table, $values)
+	{
+		$this->qb_limit = FALSE;
+		$this->qb_orderby = array();
+		return parent::_update($table, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update_Batch statement
+	 *
+	 * Generates a platform-specific batch update string from the supplied data
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$values	Update data
+	 * @param	string	$index	WHERE key
+	 * @return	string
+	 */
+	protected function _update_batch($table, $values, $index)
+	{
+		$ids = array();
+		foreach ($values as $key => $val)
+		{
+			$ids[] = $val[$index]['value'];
+
+			foreach (array_keys($val) as $field)
+			{
+				if ($field !== $index)
+				{
+					$final[$val[$field]['field']][] = 'WHEN '.$val[$index]['value'].' THEN '.$val[$field]['value'];
+				}
+			}
+		}
+
+		$cases = '';
+		foreach ($final as $k => $v)
+		{
+			$cases .= $k.' = (CASE '.$val[$index]['field']."\n"
+				.implode("\n", $v)."\n"
+				.'ELSE '.$k.' END), ';
+		}
+
+		$this->where($val[$index]['field'].' IN('.implode(',', $ids).')', NULL, FALSE);
+
+		return 'UPDATE '.$table.' SET '.substr($cases, 0, -2).$this->_compile_wh('qb_where');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete statement
+	 *
+	 * Generates a platform-specific delete string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _delete($table)
+	{
+		$this->qb_limit = FALSE;
+		return parent::_delete($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT
+	 *
+	 * Generates a platform-specific LIMIT clause
+	 *
+	 * @param	string	$sql	SQL Query
+	 * @return	string
+	 */
+	protected function _limit($sql)
+	{
+		return $sql.' LIMIT '.$this->qb_limit.($this->qb_offset ? ' OFFSET '.$this->qb_offset : '');
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php b/system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php
new file mode 100644
index 0000000..cea2054
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php
@@ -0,0 +1,218 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO PostgreSQL Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_pgsql_forge extends CI_DB_pdo_forge {
+
+	/**
+	 * DROP TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_table_if	= 'DROP TABLE IF EXISTS';
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= 'CREATE TABLE IF NOT EXISTS';
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'INT2'		=> 'INTEGER',
+		'SMALLINT'	=> 'INTEGER',
+		'INT'		=> 'BIGINT',
+		'INT4'		=> 'BIGINT',
+		'INTEGER'	=> 'BIGINT',
+		'INT8'		=> 'NUMERIC',
+		'BIGINT'	=> 'NUMERIC',
+		'REAL'		=> 'DOUBLE PRECISION',
+		'FLOAT'		=> 'DOUBLE PRECISION'
+	);
+
+	/**
+	 * NULL value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_null = 'NULL';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	object	&$db	Database object
+	 * @return	void
+	 */
+	public function __construct(&$db)
+	{
+		parent::__construct($db);
+
+		if (version_compare($this->db->version(), '9.0', '>'))
+		{
+			$this->create_table_if = 'CREATE TABLE IF NOT EXISTS';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if (in_array($alter_type, array('DROP', 'ADD'), TRUE))
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table);
+		$sqls = array();
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			if ($field[$i]['_literal'] !== FALSE)
+			{
+				return FALSE;
+			}
+
+			if (version_compare($this->db->version(), '8', '>=') && isset($field[$i]['type']))
+			{
+				$sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+					.' TYPE '.$field[$i]['type'].$field[$i]['length'];
+			}
+
+			if ( ! empty($field[$i]['default']))
+			{
+				$sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+					.' SET '.$field[$i]['default'];
+			}
+
+			if (isset($field[$i]['null']))
+			{
+				$sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+					.(trim($field[$i]['null']) === $this->_null ? ' DROP NOT NULL' : ' SET NOT NULL');
+			}
+
+			if ( ! empty($field[$i]['new_name']))
+			{
+				$sqls[] = $sql.' RENAME COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+					.' TO '.$this->db->escape_identifiers($field[$i]['new_name']);
+			}
+
+			if ( ! empty($field[$i]['comment']))
+			{
+				$sqls[] = 'COMMENT ON COLUMN '
+					.$this->db->escape_identifiers($table).'.'.$this->db->escape_identifiers($field[$i]['name'])
+					.' IS '.$field[$i]['comment'];
+			}
+		}
+
+		return $sqls;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		// Reset field lengths for data types that don't support it
+		if (isset($attributes['CONSTRAINT']) && stripos($attributes['TYPE'], 'int') !== FALSE)
+		{
+			$attributes['CONSTRAINT'] = NULL;
+		}
+
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'TINYINT':
+				$attributes['TYPE'] = 'SMALLINT';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'INTEGER';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE)
+		{
+			$field['type'] = ($field['type'] === 'NUMERIC')
+				? 'BIGSERIAL'
+				: 'SERIAL';
+		}
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_sqlite_driver.php b/system/database/drivers/pdo/subdrivers/pdo_sqlite_driver.php
new file mode 100644
index 0000000..24c34f2
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_sqlite_driver.php
@@ -0,0 +1,214 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO SQLite Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_sqlite_driver extends CI_DB_pdo_driver {
+
+	/**
+	 * Sub-driver
+	 *
+	 * @var	string
+	 */
+	public $subdriver = 'sqlite';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('RANDOM()', 'RANDOM()');
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Builds the DSN if not already set.
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if (empty($this->dsn))
+		{
+			$this->dsn = 'sqlite:';
+
+			if (empty($this->database) && empty($this->hostname))
+			{
+				$this->database = ':memory:';
+			}
+
+			$this->database = empty($this->database) ? $this->hostname : $this->database;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\'';
+
+		if ($prefix_limit === TRUE && $this->dbprefix !== '')
+		{
+			return $sql.' AND "NAME" LIKE \''.$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * @param	string	$table	Table name
+	 * @return	array
+	 */
+	public function list_fields($table)
+	{
+		if (($result = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE)
+		{
+			return FALSE;
+		}
+
+		$fields = array();
+		foreach ($result->result_array() as $row)
+		{
+			$fields[] = $row['name'];
+		}
+
+		return $fields;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		if (($query = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE)
+		{
+			return FALSE;
+		}
+
+		$query = $query->result_array();
+		if (empty($query))
+		{
+			return FALSE;
+		}
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]['name'];
+			$retval[$i]->type		= $query[$i]['type'];
+			$retval[$i]->max_length		= NULL;
+			$retval[$i]->default		= $query[$i]['dflt_value'];
+			$retval[$i]->primary_key	= isset($query[$i]['pk']) ? (int) $query[$i]['pk'] : 0;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Replace statement
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$keys	INSERT keys
+	 * @param	array	$values	INSERT values
+	 * @return 	string
+	 */
+	protected function _replace($table, $keys, $values)
+	{
+		return 'INSERT OR '.parent::_replace($table, $keys, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Truncate statement
+	 *
+	 * Generates a platform-specific truncate string from the supplied data
+	 *
+	 * If the database does not support the TRUNCATE statement,
+	 * then this method maps to 'DELETE FROM table'
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _truncate($table)
+	{
+		return 'DELETE FROM '.$table;
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_sqlite_forge.php b/system/database/drivers/pdo/subdrivers/pdo_sqlite_forge.php
new file mode 100644
index 0000000..b0edcbd
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_sqlite_forge.php
@@ -0,0 +1,239 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO SQLite Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_sqlite_forge extends CI_DB_pdo_forge {
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= 'CREATE TABLE IF NOT EXISTS';
+
+	/**
+	 * DROP TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_table_if	= 'DROP TABLE IF EXISTS';
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	bool|array
+	 */
+	protected $_unsigned		= FALSE;
+
+	/**
+	 * NULL value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_null		= 'NULL';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	object	&$db	Database object
+	 * @return	void
+	 */
+	public function __construct(&$db)
+	{
+		parent::__construct($db);
+
+		if (version_compare($this->db->version(), '3.3', '<'))
+		{
+			$this->_create_table_if = FALSE;
+			$this->_drop_table_if   = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Create database
+	 *
+	 * @param	string	$db_name	(ignored)
+	 * @return	bool
+	 */
+	public function create_database($db_name)
+	{
+		// In SQLite, a database is created when you connect to the database.
+		// We'll return TRUE so that an error isn't generated
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Drop database
+	 *
+	 * @param	string	$db_name	(ignored)
+	 * @return	bool
+	 */
+	public function drop_database($db_name)
+	{
+		// In SQLite, a database is dropped when we delete a file
+		if (file_exists($this->db->database))
+		{
+			// We need to close the pseudo-connection first
+			$this->db->close();
+			if ( ! @unlink($this->db->database))
+			{
+				return $this->db->db_debug ? $this->db->display_error('db_unable_to_drop') : FALSE;
+			}
+			elseif ( ! empty($this->db->data_cache['db_names']))
+			{
+				$key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE);
+				if ($key !== FALSE)
+				{
+					unset($this->db->data_cache['db_names'][$key]);
+				}
+			}
+
+			return TRUE;
+		}
+
+		return $this->db->db_debug ? $this->db->display_error('db_unable_to_drop') : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if ($alter_type === 'DROP' OR $alter_type === 'CHANGE')
+		{
+			// drop_column():
+			//	BEGIN TRANSACTION;
+			//	CREATE TEMPORARY TABLE t1_backup(a,b);
+			//	INSERT INTO t1_backup SELECT a,b FROM t1;
+			//	DROP TABLE t1;
+			//	CREATE TABLE t1(a,b);
+			//	INSERT INTO t1 SELECT a,b FROM t1_backup;
+			//	DROP TABLE t1_backup;
+			//	COMMIT;
+
+			return FALSE;
+		}
+
+		return parent::_alter_table($alter_type, $table, $field);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process column
+	 *
+	 * @param	array	$field
+	 * @return	string
+	 */
+	protected function _process_column($field)
+	{
+		return $this->db->escape_identifiers($field['name'])
+			.' '.$field['type']
+			.$field['auto_increment']
+			.$field['null']
+			.$field['unique']
+			.$field['default'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'ENUM':
+			case 'SET':
+				$attributes['TYPE'] = 'TEXT';
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE)
+		{
+			$field['type'] = 'INTEGER PRIMARY KEY';
+			$field['default'] = '';
+			$field['null'] = '';
+			$field['unique'] = '';
+			$field['auto_increment'] = ' AUTOINCREMENT';
+
+			$this->primary_keys = array();
+		}
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_driver.php b/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_driver.php
new file mode 100644
index 0000000..685b61e
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_driver.php
@@ -0,0 +1,370 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO SQLSRV Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_sqlsrv_driver extends CI_DB_pdo_driver {
+
+	/**
+	 * Sub-driver
+	 *
+	 * @var	string
+	 */
+	public $subdriver = 'sqlsrv';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('NEWID()', 'RAND(%d)');
+
+	/**
+	 * Quoted identifier flag
+	 *
+	 * Whether to use SQL-92 standard quoted identifier
+	 * (double quotes) or brackets for identifier escaping.
+	 *
+	 * @var	bool
+	 */
+	protected $_quoted_identifier;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * Builds the DSN if not already set.
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		if (empty($this->dsn))
+		{
+			$this->dsn = 'sqlsrv:Server='.(empty($this->hostname) ? '127.0.0.1' : $this->hostname);
+
+			empty($this->port) OR $this->dsn .= ','.$this->port;
+			empty($this->database) OR $this->dsn .= ';Database='.$this->database;
+
+			// Some custom options
+
+			if (isset($this->QuotedId))
+			{
+				$this->dsn .= ';QuotedId='.$this->QuotedId;
+				$this->_quoted_identifier = (bool) $this->QuotedId;
+			}
+
+			if (isset($this->ConnectionPooling))
+			{
+				$this->dsn .= ';ConnectionPooling='.$this->ConnectionPooling;
+			}
+
+			if ($this->encrypt === TRUE)
+			{
+				$this->dsn .= ';Encrypt=1';
+			}
+
+			if (isset($this->TraceOn))
+			{
+				$this->dsn .= ';TraceOn='.$this->TraceOn;
+			}
+
+			if (isset($this->TrustServerCertificate))
+			{
+				$this->dsn .= ';TrustServerCertificate='.$this->TrustServerCertificate;
+			}
+
+			empty($this->APP) OR $this->dsn .= ';APP='.$this->APP;
+			empty($this->Failover_Partner) OR $this->dsn .= ';Failover_Partner='.$this->Failover_Partner;
+			empty($this->LoginTimeout) OR $this->dsn .= ';LoginTimeout='.$this->LoginTimeout;
+			empty($this->MultipleActiveResultSets) OR $this->dsn .= ';MultipleActiveResultSets='.$this->MultipleActiveResultSets;
+			empty($this->TraceFile) OR $this->dsn .= ';TraceFile='.$this->TraceFile;
+			empty($this->WSID) OR $this->dsn .= ';WSID='.$this->WSID;
+		}
+		elseif (preg_match('/QuotedId=(0|1)/', $this->dsn, $match))
+		{
+			$this->_quoted_identifier = (bool) $match[1];
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	object
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		if ( ! empty($this->char_set) && preg_match('/utf[^8]*8/i', $this->char_set))
+		{
+			$this->options[PDO::SQLSRV_ENCODING_UTF8] = 1;
+		}
+
+		$this->conn_id = parent::db_connect($persistent);
+
+		if ( ! is_object($this->conn_id) OR is_bool($this->_quoted_identifier))
+		{
+			return $this->conn_id;
+		}
+
+		// Determine how identifiers are escaped
+		$query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi');
+		$query = $query->row_array();
+		$this->_quoted_identifier = empty($query) ? FALSE : (bool) $query['qi'];
+		$this->_escape_char = ($this->_quoted_identifier) ? '"' : array('[', ']');
+
+		return $this->conn_id;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SELECT '.$this->escape_identifiers('name')
+			.' FROM '.$this->escape_identifiers('sysobjects')
+			.' WHERE '.$this->escape_identifiers('type')." = 'U'";
+
+		if ($prefix_limit === TRUE && $this->dbprefix !== '')
+		{
+			$sql .= ' AND '.$this->escape_identifiers('name')." LIKE '".$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql.' ORDER BY '.$this->escape_identifiers('name');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SELECT COLUMN_NAME
+			FROM INFORMATION_SCHEMA.Columns
+			WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		$sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT
+			FROM INFORMATION_SCHEMA.Columns
+			WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
+
+		if (($query = $this->query($sql)) === FALSE)
+		{
+			return FALSE;
+		}
+		$query = $query->result_object();
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]->COLUMN_NAME;
+			$retval[$i]->type		= $query[$i]->DATA_TYPE;
+			$retval[$i]->max_length		= ($query[$i]->CHARACTER_MAXIMUM_LENGTH > 0) ? $query[$i]->CHARACTER_MAXIMUM_LENGTH : $query[$i]->NUMERIC_PRECISION;
+			$retval[$i]->default		= $query[$i]->COLUMN_DEFAULT;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update statement
+	 *
+	 * Generates a platform-specific update string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @param	array	$values
+	 * @return	string
+	 */
+	protected function _update($table, $values)
+	{
+		$this->qb_limit = FALSE;
+		$this->qb_orderby = array();
+		return parent::_update($table, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete statement
+	 *
+	 * Generates a platform-specific delete string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _delete($table)
+	{
+		if ($this->qb_limit)
+		{
+			return 'WITH ci_delete AS (SELECT TOP '.$this->qb_limit.' * FROM '.$table.$this->_compile_wh('qb_where').') DELETE FROM ci_delete';
+		}
+
+		return parent::_delete($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT
+	 *
+	 * Generates a platform-specific LIMIT clause
+	 *
+	 * @param	string	$sql	SQL Query
+	 * @return	string
+	 */
+	protected function _limit($sql)
+	{
+		// As of SQL Server 2012 (11.0.*) OFFSET is supported
+		if (version_compare($this->version(), '11', '>='))
+		{
+			// SQL Server OFFSET-FETCH can be used only with the ORDER BY clause
+			empty($this->qb_orderby) && $sql .= ' ORDER BY 1';
+
+			return $sql.' OFFSET '.(int) $this->qb_offset.' ROWS FETCH NEXT '.$this->qb_limit.' ROWS ONLY';
+		}
+
+		$limit = $this->qb_offset + $this->qb_limit;
+
+		// An ORDER BY clause is required for ROW_NUMBER() to work
+		if ($this->qb_offset && ! empty($this->qb_orderby))
+		{
+			$orderby = $this->_compile_order_by();
+
+			// We have to strip the ORDER BY clause
+			$sql = trim(substr($sql, 0, strrpos($sql, $orderby)));
+
+			// Get the fields to select from our subquery, so that we can avoid CI_rownum appearing in the actual results
+			if (count($this->qb_select) === 0 OR strpos(implode(',', $this->qb_select), '*') !== FALSE)
+			{
+				$select = '*'; // Inevitable
+			}
+			else
+			{
+				// Use only field names and their aliases, everything else is out of our scope.
+				$select = array();
+				$field_regexp = ($this->_quoted_identifier)
+					? '("[^\"]+")' : '(\[[^\]]+\])';
+				for ($i = 0, $c = count($this->qb_select); $i < $c; $i++)
+				{
+					$select[] = preg_match('/(?:\s|\.)'.$field_regexp.'$/i', $this->qb_select[$i], $m)
+						? $m[1] : $this->qb_select[$i];
+				}
+				$select = implode(', ', $select);
+			}
+
+			return 'SELECT '.$select." FROM (\n\n"
+				.preg_replace('/^(SELECT( DISTINCT)?)/i', '\\1 ROW_NUMBER() OVER('.trim($orderby).') AS '.$this->escape_identifiers('CI_rownum').', ', $sql)
+				."\n\n) ".$this->escape_identifiers('CI_subquery')
+				."\nWHERE ".$this->escape_identifiers('CI_rownum').' BETWEEN '.($this->qb_offset + 1).' AND '.$limit;
+		}
+
+		return preg_replace('/(^\SELECT (DISTINCT)?)/i','\\1 TOP '.$limit.' ', $sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert batch statement
+	 *
+	 * Generates a platform-specific insert string from the supplied data.
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$keys	INSERT keys
+	 * @param	array	$values	INSERT values
+	 * @return	string|bool
+	 */
+	protected function _insert_batch($table, $keys, $values)
+	{
+		// Multiple-value inserts are only supported as of SQL Server 2008
+		if (version_compare($this->version(), '10', '>='))
+		{
+			return parent::_insert_batch($table, $keys, $values);
+		}
+
+		return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE;
+	}
+
+}
diff --git a/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_forge.php b/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_forge.php
new file mode 100644
index 0000000..07eecea
--- /dev/null
+++ b/system/database/drivers/pdo/subdrivers/pdo_sqlsrv_forge.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PDO SQLSRV Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_pdo_sqlsrv_forge extends CI_DB_pdo_forge {
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= "IF NOT EXISTS (SELECT * FROM sysobjects WHERE ID = object_id(N'%s') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)\nCREATE TABLE";
+
+	/**
+	 * DROP TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_table_if	= "IF EXISTS (SELECT * FROM sysobjects WHERE ID = object_id(N'%s') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)\nDROP TABLE";
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'TINYINT'	=> 'SMALLINT',
+		'SMALLINT'	=> 'INT',
+		'INT'		=> 'BIGINT',
+		'REAL'		=> 'FLOAT'
+	);
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if (in_array($alter_type, array('ADD', 'DROP'), TRUE))
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' ALTER COLUMN ';
+		$sqls = array();
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			$sqls[] = $sql.$this->_process_column($field[$i]);
+		}
+
+		return $sqls;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		if (isset($attributes['CONSTRAINT']) && strpos($attributes['TYPE'], 'INT') !== FALSE)
+		{
+			unset($attributes['CONSTRAINT']);
+		}
+
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'INTEGER';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'INTEGER':
+				$attributes['TYPE'] = 'INT';
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE)
+		{
+			$field['auto_increment'] = ' IDENTITY(1,1)';
+		}
+	}
+
+}
diff --git a/system/database/drivers/postgre/index.html b/system/database/drivers/postgre/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/drivers/postgre/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/system/database/drivers/postgre/postgre_driver.php b/system/database/drivers/postgre/postgre_driver.php
new file mode 100644
index 0000000..15d800b
--- /dev/null
+++ b/system/database/drivers/postgre/postgre_driver.php
@@ -0,0 +1,611 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Postgre Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_postgre_driver extends CI_DB {
+
+	/**
+	 * Database driver
+	 *
+	 * @var	string
+	 */
+	public $dbdriver = 'postgre';
+
+	/**
+	 * Database schema
+	 *
+	 * @var	string
+	 */
+	public $schema = 'public';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('RANDOM()', 'RANDOM()');
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Build DSN
+	 *
+	 * @return	void
+	 */
+	protected function _build_dsn()
+	{
+		$this->dsn === '' OR $this->dsn = '';
+
+		if (strpos($this->hostname, '/') !== FALSE)
+		{
+			// If UNIX sockets are used, we shouldn't set a port
+			$this->port = '';
+		}
+
+		$this->hostname === '' OR $this->dsn = 'host='.$this->hostname.' ';
+
+		if ( ! empty($this->port) && ctype_digit($this->port))
+		{
+			$this->dsn .= 'port='.$this->port.' ';
+		}
+
+		if ($this->username !== '')
+		{
+			$this->dsn .= 'user='.$this->username.' ';
+
+			/* An empty password is valid!
+			 *
+			 * $db['password'] = NULL must be done in order to ignore it.
+			 */
+			$this->password === NULL OR $this->dsn .= "password='".$this->password."' ";
+		}
+
+		$this->database === '' OR $this->dsn .= 'dbname='.$this->database.' ';
+
+		/* We don't have these options as elements in our standard configuration
+		 * array, but they might be set by parse_url() if the configuration was
+		 * provided via string. Example:
+		 *
+		 * postgre://username:password@localhost:5432/database?connect_timeout=5&sslmode=1
+		 */
+		foreach (array('connect_timeout', 'options', 'sslmode', 'service') as $key)
+		{
+			if (isset($this->$key) && is_string($this->$key) && $this->$key !== '')
+			{
+				$this->dsn .= $key."='".$this->$key."' ";
+			}
+		}
+
+		$this->dsn = rtrim($this->dsn);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	resource|object
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		empty($this->dsn) && $this->_build_dsn();
+		$this->conn_id = ($persistent === TRUE)
+			? pg_pconnect($this->dsn)
+			: pg_connect($this->dsn);
+
+		if ($this->conn_id !== FALSE)
+		{
+			if ($persistent === TRUE
+				&& pg_connection_status($this->conn_id) === PGSQL_CONNECTION_BAD
+				&& pg_ping($this->conn_id) === FALSE
+			)
+			{
+				return FALSE;
+			}
+
+			empty($this->schema) OR $this->simple_query('SET search_path TO '.$this->schema.',public');
+		}
+
+		return $this->conn_id;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Reconnect
+	 *
+	 * Keep / reestablish the db connection if no queries have been
+	 * sent for a length of time exceeding the server's idle timeout
+	 *
+	 * @return	void
+	 */
+	public function reconnect()
+	{
+		if (pg_ping($this->conn_id) === FALSE)
+		{
+			$this->conn_id = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set client character set
+	 *
+	 * @param	string	$charset
+	 * @return	bool
+	 */
+	protected function _db_set_charset($charset)
+	{
+		return (pg_set_client_encoding($this->conn_id, $charset) === 0);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database version number
+	 *
+	 * @return	string
+	 */
+	public function version()
+	{
+		if (isset($this->data_cache['version']))
+		{
+			return $this->data_cache['version'];
+		}
+
+		if ( ! $this->conn_id OR ($pg_version = pg_version($this->conn_id)) === FALSE)
+		{
+			return FALSE;
+		}
+
+		/* If PHP was compiled with PostgreSQL lib versions earlier
+		 * than 7.4, pg_version() won't return the server version
+		 * and so we'll have to fall back to running a query in
+		 * order to get it.
+		 */
+		return (isset($pg_version['server']) && preg_match('#^(\d+\.\d+)#', $pg_version['server'], $match))
+			? $this->data_cache['version'] = $match[1]
+			: parent::version();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Execute the query
+	 *
+	 * @param	string	$sql	an SQL query
+	 * @return	resource|object
+	 */
+	protected function _execute($sql)
+	{
+		return pg_query($this->conn_id, $sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Begin Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_begin()
+	{
+		return (bool) pg_query($this->conn_id, 'BEGIN');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Commit Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_commit()
+	{
+		return (bool) pg_query($this->conn_id, 'COMMIT');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rollback Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_rollback()
+	{
+		return (bool) pg_query($this->conn_id, 'ROLLBACK');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Determines if a query is a "write" type.
+	 *
+	 * @param	string	An SQL query string
+	 * @return	bool
+	 */
+	public function is_write_type($sql)
+	{
+		if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql))
+		{
+			return FALSE;
+		}
+
+		return parent::is_write_type($sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Platform-dependent string escape
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _escape_str($str)
+	{
+		return pg_escape_string($this->conn_id, $str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * "Smart" Escape String
+	 *
+	 * Escapes data based on type
+	 *
+	 * @param	string	$str
+	 * @return	mixed
+	 */
+	public function escape($str)
+	{
+		if (is_php('5.4.4') && (is_string($str) OR (is_object($str) && method_exists($str, '__toString'))))
+		{
+			return pg_escape_literal($this->conn_id, $str);
+		}
+		elseif (is_bool($str))
+		{
+			return ($str) ? 'TRUE' : 'FALSE';
+		}
+
+		return parent::escape($str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Affected Rows
+	 *
+	 * @return	int
+	 */
+	public function affected_rows()
+	{
+		return pg_affected_rows($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert ID
+	 *
+	 * @return	string
+	 */
+	public function insert_id()
+	{
+		$v = $this->version();
+
+		$table	= (func_num_args() > 0) ? func_get_arg(0) : NULL;
+		$column	= (func_num_args() > 1) ? func_get_arg(1) : NULL;
+
+		if ($table === NULL && $v >= '8.1')
+		{
+			$sql = 'SELECT LASTVAL() AS ins_id';
+		}
+		elseif ($table !== NULL)
+		{
+			if ($column !== NULL && $v >= '8.0')
+			{
+				$sql = 'SELECT pg_get_serial_sequence(\''.$table."', '".$column."') AS seq";
+				$query = $this->query($sql);
+				$query = $query->row();
+				$seq = $query->seq;
+			}
+			else
+			{
+				// seq_name passed in table parameter
+				$seq = $table;
+			}
+
+			$sql = 'SELECT CURRVAL(\''.$seq."') AS ins_id";
+		}
+		else
+		{
+			return pg_last_oid($this->result_id);
+		}
+
+		$query = $this->query($sql);
+		$query = $query->row();
+		return (int) $query->ins_id;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \''.$this->schema."'";
+
+		if ($prefix_limit !== FALSE && $this->dbprefix !== '')
+		{
+			return $sql.' AND "table_name" LIKE \''
+				.$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * List column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SELECT "column_name"
+			FROM "information_schema"."columns"
+			WHERE "table_schema" = \''.$this->schema.'\' AND LOWER("table_name") = '.$this->escape(strtolower($table));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		$sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default"
+			FROM "information_schema"."columns"
+			WHERE "table_schema" = \''.$this->schema.'\' AND LOWER("table_name") = '.$this->escape(strtolower($table));
+
+		if (($query = $this->query($sql)) === FALSE)
+		{
+			return FALSE;
+		}
+		$query = $query->result_object();
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]->column_name;
+			$retval[$i]->type		= $query[$i]->data_type;
+			$retval[$i]->max_length		= ($query[$i]->character_maximum_length > 0) ? $query[$i]->character_maximum_length : $query[$i]->numeric_precision;
+			$retval[$i]->default		= $query[$i]->column_default;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error
+	 *
+	 * Returns an array containing code and message of the last
+	 * database error that has occurred.
+	 *
+	 * @return	array
+	 */
+	public function error()
+	{
+		return array('code' => '', 'message' => pg_last_error($this->conn_id));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY
+	 *
+	 * @param	string	$orderby
+	 * @param	string	$direction	ASC, DESC or RANDOM
+	 * @param	bool	$escape
+	 * @return	object
+	 */
+	public function order_by($orderby, $direction = '', $escape = NULL)
+	{
+		$direction = strtoupper(trim($direction));
+		if ($direction === 'RANDOM')
+		{
+			if ( ! is_float($orderby) && ctype_digit((string) $orderby))
+			{
+				$orderby = ($orderby > 1)
+					? (float) '0.'.$orderby
+					: (float) $orderby;
+			}
+
+			if (is_float($orderby))
+			{
+				$this->simple_query('SET SEED '.$orderby);
+			}
+
+			$orderby = $this->_random_keyword[0];
+			$direction = '';
+			$escape = FALSE;
+		}
+
+		return parent::order_by($orderby, $direction, $escape);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update statement
+	 *
+	 * Generates a platform-specific update string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @param	array	$values
+	 * @return	string
+	 */
+	protected function _update($table, $values)
+	{
+		$this->qb_limit = FALSE;
+		$this->qb_orderby = array();
+		return parent::_update($table, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update_Batch statement
+	 *
+	 * Generates a platform-specific batch update string from the supplied data
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$values	Update data
+	 * @param	string	$index	WHERE key
+	 * @return	string
+	 */
+	protected function _update_batch($table, $values, $index)
+	{
+		$ids = array();
+		foreach ($values as $key => $val)
+		{
+			$ids[] = $val[$index]['value'];
+
+			foreach (array_keys($val) as $field)
+			{
+				if ($field !== $index)
+				{
+					$final[$val[$field]['field']][] = 'WHEN '.$val[$index]['value'].' THEN '.$val[$field]['value'];
+				}
+			}
+		}
+
+		$cases = '';
+		foreach ($final as $k => $v)
+		{
+			$cases .= $k.' = (CASE '.$val[$index]['field']."\n"
+				.implode("\n", $v)."\n"
+				.'ELSE '.$k.' END), ';
+		}
+
+		$this->where($val[$index]['field'].' IN('.implode(',', $ids).')', NULL, FALSE);
+
+		return 'UPDATE '.$table.' SET '.substr($cases, 0, -2).$this->_compile_wh('qb_where');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete statement
+	 *
+	 * Generates a platform-specific delete string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _delete($table)
+	{
+		$this->qb_limit = FALSE;
+		return parent::_delete($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT
+	 *
+	 * Generates a platform-specific LIMIT clause
+	 *
+	 * @param	string	$sql	SQL Query
+	 * @return	string
+	 */
+	protected function _limit($sql)
+	{
+		return $sql.' LIMIT '.$this->qb_limit.($this->qb_offset ? ' OFFSET '.$this->qb_offset : '');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close DB Connection
+	 *
+	 * @return	void
+	 */
+	protected function _close()
+	{
+		pg_close($this->conn_id);
+	}
+
+}
diff --git a/system/database/drivers/postgre/postgre_forge.php b/system/database/drivers/postgre/postgre_forge.php
new file mode 100644
index 0000000..2857fd5
--- /dev/null
+++ b/system/database/drivers/postgre/postgre_forge.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) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Postgre Forge Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_postgre_forge extends CI_DB_forge {
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'INT2'		=> 'INTEGER',
+		'SMALLINT'	=> 'INTEGER',
+		'INT'		=> 'BIGINT',
+		'INT4'		=> 'BIGINT',
+		'INTEGER'	=> 'BIGINT',
+		'INT8'		=> 'NUMERIC',
+		'BIGINT'	=> 'NUMERIC',
+		'REAL'		=> 'DOUBLE PRECISION',
+		'FLOAT'		=> 'DOUBLE PRECISION'
+	);
+
+	/**
+	 * NULL value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_null = 'NULL';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	object	&$db	Database object
+	 * @return	void
+	 */
+	public function __construct(&$db)
+	{
+		parent::__construct($db);
+
+		if (version_compare($this->db->version(), '9.0', '>'))
+		{
+			$this->create_table_if = 'CREATE TABLE IF NOT EXISTS';
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if (in_array($alter_type, array('DROP', 'ADD'), TRUE))
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table);
+		$sqls = array();
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			if ($field[$i]['_literal'] !== FALSE)
+			{
+				return FALSE;
+			}
+
+			if (version_compare($this->db->version(), '8', '>=') && isset($field[$i]['type']))
+			{
+				$sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+					.' TYPE '.$field[$i]['type'].$field[$i]['length'];
+			}
+
+			if ( ! empty($field[$i]['default']))
+			{
+				$sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+					.' SET '.$field[$i]['default'];
+			}
+
+			if (isset($field[$i]['null']))
+			{
+				$sqls[] = $sql.' ALTER COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+					.(trim($field[$i]['null']) === $this->_null ? ' DROP NOT NULL' : ' SET NOT NULL');
+			}
+
+			if ( ! empty($field[$i]['new_name']))
+			{
+				$sqls[] = $sql.' RENAME COLUMN '.$this->db->escape_identifiers($field[$i]['name'])
+					.' TO '.$this->db->escape_identifiers($field[$i]['new_name']);
+			}
+
+			if ( ! empty($field[$i]['comment']))
+			{
+				$sqls[] = 'COMMENT ON COLUMN '
+					.$this->db->escape_identifiers($table).'.'.$this->db->escape_identifiers($field[$i]['name'])
+					.' IS '.$field[$i]['comment'];
+			}
+		}
+
+		return $sqls;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		// Reset field lengths for data types that don't support it
+		if (isset($attributes['CONSTRAINT']) && stripos($attributes['TYPE'], 'int') !== FALSE)
+		{
+			$attributes['CONSTRAINT'] = NULL;
+		}
+
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'TINYINT':
+				$attributes['TYPE'] = 'SMALLINT';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'INTEGER';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE)
+		{
+			$field['type'] = ($field['type'] === 'NUMERIC')
+				? 'BIGSERIAL'
+				: 'SERIAL';
+		}
+	}
+
+}
diff --git a/system/database/drivers/postgre/postgre_result.php b/system/database/drivers/postgre/postgre_result.php
new file mode 100644
index 0000000..5e4145e
--- /dev/null
+++ b/system/database/drivers/postgre/postgre_result.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Postgres Result Class
+ *
+ * This class extends the parent result class: CI_DB_result
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_postgre_result extends CI_DB_result {
+
+	/**
+	 * Number of rows in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_rows()
+	{
+		return is_int($this->num_rows)
+			? $this->num_rows
+			: $this->num_rows = pg_num_rows($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Number of fields in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_fields()
+	{
+		return pg_num_fields($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * Generates an array of column names
+	 *
+	 * @return	array
+	 */
+	public function list_fields()
+	{
+		$field_names = array();
+		for ($i = 0, $c = $this->num_fields(); $i < $c; $i++)
+		{
+			$field_names[] = pg_field_name($this->result_id, $i);
+		}
+
+		return $field_names;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data
+	 *
+	 * Generates an array of objects containing field meta-data
+	 *
+	 * @return	array
+	 */
+	public function field_data()
+	{
+		$retval = array();
+		for ($i = 0, $c = $this->num_fields(); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= pg_field_name($this->result_id, $i);
+			$retval[$i]->type		= pg_field_type($this->result_id, $i);
+			$retval[$i]->max_length		= pg_field_size($this->result_id, $i);
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Free the result
+	 *
+	 * @return	void
+	 */
+	public function free_result()
+	{
+		if ($this->result_id !== FALSE)
+		{
+			pg_free_result($this->result_id);
+			$this->result_id = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Data Seek
+	 *
+	 * Moves the internal pointer to the desired offset. We call
+	 * this internally before fetching results to make sure the
+	 * result set starts at zero.
+	 *
+	 * @param	int	$n
+	 * @return	bool
+	 */
+	public function data_seek($n = 0)
+	{
+		return pg_result_seek($this->result_id, $n);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - associative array
+	 *
+	 * Returns the result set as an array
+	 *
+	 * @return	array
+	 */
+	protected function _fetch_assoc()
+	{
+		return pg_fetch_assoc($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - object
+	 *
+	 * Returns the result set as an object
+	 *
+	 * @param	string	$class_name
+	 * @return	object
+	 */
+	protected function _fetch_object($class_name = 'stdClass')
+	{
+		return pg_fetch_object($this->result_id, NULL, $class_name);
+	}
+
+}
diff --git a/system/database/drivers/postgre/postgre_utility.php b/system/database/drivers/postgre/postgre_utility.php
new file mode 100644
index 0000000..c8356d5
--- /dev/null
+++ b/system/database/drivers/postgre/postgre_utility.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * Postgre Utility Class
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_postgre_utility extends CI_DB_utility {
+
+	/**
+	 * List databases statement
+	 *
+	 * @var	string
+	 */
+	protected $_list_databases	= 'SELECT datname FROM pg_database';
+
+	/**
+	 * OPTIMIZE TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_optimize_table	= 'REINDEX TABLE %s';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Export
+	 *
+	 * @param	array	$params	Preferences
+	 * @return	mixed
+	 */
+	protected function _backup($params = array())
+	{
+		// Currently unsupported
+		return $this->db->display_error('db_unsupported_feature');
+	}
+}
diff --git a/system/database/drivers/sqlite/index.html b/system/database/drivers/sqlite/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/drivers/sqlite/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/system/database/drivers/sqlite/sqlite_driver.php b/system/database/drivers/sqlite/sqlite_driver.php
new file mode 100644
index 0000000..188f00c
--- /dev/null
+++ b/system/database/drivers/sqlite/sqlite_driver.php
@@ -0,0 +1,331 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * SQLite Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_sqlite_driver extends CI_DB {
+
+	/**
+	 * Database driver
+	 *
+	 * @var	string
+	 */
+	public $dbdriver = 'sqlite';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('RANDOM()', 'RANDOM()');
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Non-persistent database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	resource
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		$error = NULL;
+		$conn_id = ($persistent === TRUE)
+			? sqlite_popen($this->database, 0666, $error)
+			: sqlite_open($this->database, 0666, $error);
+
+		isset($error) && log_message('error', $error);
+
+		return $conn_id;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database version number
+	 *
+	 * @return	string
+	 */
+	public function version()
+	{
+		return isset($this->data_cache['version'])
+			? $this->data_cache['version']
+			: $this->data_cache['version'] = sqlite_libversion();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Execute the query
+	 *
+	 * @param	string	$sql	an SQL query
+	 * @return	resource
+	 */
+	protected function _execute($sql)
+	{
+		return $this->is_write_type($sql)
+			? sqlite_exec($this->conn_id, $sql)
+			: sqlite_query($this->conn_id, $sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Begin Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_begin()
+	{
+		return $this->simple_query('BEGIN TRANSACTION');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Commit Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_commit()
+	{
+		return $this->simple_query('COMMIT');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rollback Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_rollback()
+	{
+		return $this->simple_query('ROLLBACK');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Platform-dependant string escape
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _escape_str($str)
+	{
+		return sqlite_escape_string($str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Affected Rows
+	 *
+	 * @return	int
+	 */
+	public function affected_rows()
+	{
+		return sqlite_changes($this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert ID
+	 *
+	 * @return	int
+	 */
+	public function insert_id()
+	{
+		return sqlite_last_insert_rowid($this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * List table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = "SELECT name FROM sqlite_master WHERE type='table'";
+
+		if ($prefix_limit !== FALSE && $this->dbprefix != '')
+		{
+			return $sql." AND 'name' LIKE '".$this->escape_like_str($this->dbprefix)."%' ".sprintf($this->_like_escape_str, $this->_like_escape_chr);
+		}
+
+		return $sql;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	bool
+	 */
+	protected function _list_columns($table = '')
+	{
+		// Not supported
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		if (($query = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE)
+		{
+			return FALSE;
+		}
+
+		$query = $query->result_array();
+		if (empty($query))
+		{
+			return FALSE;
+		}
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]['name'];
+			$retval[$i]->type		= $query[$i]['type'];
+			$retval[$i]->max_length		= NULL;
+			$retval[$i]->default		= $query[$i]['dflt_value'];
+			$retval[$i]->primary_key	= isset($query[$i]['pk']) ? (int) $query[$i]['pk'] : 0;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error
+	 *
+	 * Returns an array containing code and message of the last
+	 * database error that has occured.
+	 *
+	 * @return	array
+	 */
+	public function error()
+	{
+		$error = array('code' => sqlite_last_error($this->conn_id));
+		$error['message'] = sqlite_error_string($error['code']);
+		return $error;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Replace statement
+	 *
+	 * Generates a platform-specific replace string from the supplied data
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$keys	INSERT keys
+	 * @param	array	$values	INSERT values
+	 * @return	string
+	 */
+	protected function _replace($table, $keys, $values)
+	{
+		return 'INSERT OR '.parent::_replace($table, $keys, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Truncate statement
+	 *
+	 * Generates a platform-specific truncate string from the supplied data
+	 *
+	 * If the database does not support the TRUNCATE statement,
+	 * then this function maps to 'DELETE FROM table'
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _truncate($table)
+	{
+		return 'DELETE FROM '.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close DB Connection
+	 *
+	 * @return	void
+	 */
+	protected function _close()
+	{
+		sqlite_close($this->conn_id);
+	}
+
+}
diff --git a/system/database/drivers/sqlite/sqlite_forge.php b/system/database/drivers/sqlite/sqlite_forge.php
new file mode 100644
index 0000000..60aaa09
--- /dev/null
+++ b/system/database/drivers/sqlite/sqlite_forge.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) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * SQLite Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_sqlite_forge extends CI_DB_forge {
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= FALSE;
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	bool|array
+	 */
+	protected $_unsigned		= FALSE;
+
+	/**
+	 * NULL value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_null		= 'NULL';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Create database
+	 *
+	 * @param	string	$db_name	(ignored)
+	 * @return	bool
+	 */
+	public function create_database($db_name)
+	{
+		// In SQLite, a database is created when you connect to the database.
+		// We'll return TRUE so that an error isn't generated
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Drop database
+	 *
+	 * @param	string	$db_name	(ignored)
+	 * @return	bool
+	 */
+	public function drop_database($db_name)
+	{
+		if ( ! file_exists($this->db->database) OR ! @unlink($this->db->database))
+		{
+			return ($this->db->db_debug) ? $this->db->display_error('db_unable_to_drop') : FALSE;
+		}
+		elseif ( ! empty($this->db->data_cache['db_names']))
+		{
+			$key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE);
+			if ($key !== FALSE)
+			{
+				unset($this->db->data_cache['db_names'][$key]);
+			}
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @todo	implement drop_column(), modify_column()
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if ($alter_type === 'DROP' OR $alter_type === 'CHANGE')
+		{
+			// drop_column():
+			//	BEGIN TRANSACTION;
+			//	CREATE TEMPORARY TABLE t1_backup(a,b);
+			//	INSERT INTO t1_backup SELECT a,b FROM t1;
+			//	DROP TABLE t1;
+			//	CREATE TABLE t1(a,b);
+			//	INSERT INTO t1 SELECT a,b FROM t1_backup;
+			//	DROP TABLE t1_backup;
+			//	COMMIT;
+
+			return FALSE;
+		}
+
+		return parent::_alter_table($alter_type, $table, $field);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process column
+	 *
+	 * @param	array	$field
+	 * @return	string
+	 */
+	protected function _process_column($field)
+	{
+		return $this->db->escape_identifiers($field['name'])
+			.' '.$field['type']
+			.$field['auto_increment']
+			.$field['null']
+			.$field['unique']
+			.$field['default'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'ENUM':
+			case 'SET':
+				$attributes['TYPE'] = 'TEXT';
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE)
+		{
+			$field['type'] = 'INTEGER PRIMARY KEY';
+			$field['default'] = '';
+			$field['null'] = '';
+			$field['unique'] = '';
+			$field['auto_increment'] = ' AUTOINCREMENT';
+
+			$this->primary_keys = array();
+		}
+	}
+
+}
diff --git a/system/database/drivers/sqlite/sqlite_result.php b/system/database/drivers/sqlite/sqlite_result.php
new file mode 100644
index 0000000..1df9025
--- /dev/null
+++ b/system/database/drivers/sqlite/sqlite_result.php
@@ -0,0 +1,165 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * SQLite Result Class
+ *
+ * This class extends the parent result class: CI_DB_result
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_sqlite_result extends CI_DB_result {
+
+	/**
+	 * Number of rows in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_rows()
+	{
+		return is_int($this->num_rows)
+			? $this->num_rows
+			: $this->num_rows = @sqlite_num_rows($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Number of fields in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_fields()
+	{
+		return @sqlite_num_fields($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * Generates an array of column names
+	 *
+	 * @return	array
+	 */
+	public function list_fields()
+	{
+		$field_names = array();
+		for ($i = 0, $c = $this->num_fields(); $i < $c; $i++)
+		{
+			$field_names[$i] = sqlite_field_name($this->result_id, $i);
+		}
+
+		return $field_names;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data
+	 *
+	 * Generates an array of objects containing field meta-data
+	 *
+	 * @return	array
+	 */
+	public function field_data()
+	{
+		$retval = array();
+		for ($i = 0, $c = $this->num_fields(); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= sqlite_field_name($this->result_id, $i);
+			$retval[$i]->type		= NULL;
+			$retval[$i]->max_length		= NULL;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Data Seek
+	 *
+	 * Moves the internal pointer to the desired offset. We call
+	 * this internally before fetching results to make sure the
+	 * result set starts at zero.
+	 *
+	 * @param	int	$n
+	 * @return	bool
+	 */
+	public function data_seek($n = 0)
+	{
+		return sqlite_seek($this->result_id, $n);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - associative array
+	 *
+	 * Returns the result set as an array
+	 *
+	 * @return	array
+	 */
+	protected function _fetch_assoc()
+	{
+		return sqlite_fetch_array($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - object
+	 *
+	 * Returns the result set as an object
+	 *
+	 * @param	string	$class_name
+	 * @return	object
+	 */
+	protected function _fetch_object($class_name = 'stdClass')
+	{
+		return sqlite_fetch_object($this->result_id, $class_name);
+	}
+
+}
diff --git a/system/database/drivers/sqlite/sqlite_utility.php b/system/database/drivers/sqlite/sqlite_utility.php
new file mode 100644
index 0000000..5f9adf2
--- /dev/null
+++ b/system/database/drivers/sqlite/sqlite_utility.php
@@ -0,0 +1,62 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * SQLite Utility Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_sqlite_utility extends CI_DB_utility {
+
+	/**
+	 * Export
+	 *
+	 * @param	array	$params	Preferences
+	 * @return	mixed
+	 */
+	protected function _backup($params = array())
+	{
+		// Currently unsupported
+		return $this->db->display_error('db_unsupported_feature');
+	}
+
+}
diff --git a/system/database/drivers/sqlite3/index.html b/system/database/drivers/sqlite3/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/drivers/sqlite3/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/system/database/drivers/sqlite3/sqlite3_driver.php b/system/database/drivers/sqlite3/sqlite3_driver.php
new file mode 100644
index 0000000..be79ddd
--- /dev/null
+++ b/system/database/drivers/sqlite3/sqlite3_driver.php
@@ -0,0 +1,345 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * SQLite3 Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		Andrey Andreev
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_sqlite3_driver extends CI_DB {
+
+	/**
+	 * Database driver
+	 *
+	 * @var	string
+	 */
+	public $dbdriver = 'sqlite3';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('RANDOM()', 'RANDOM()');
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Non-persistent database connection
+	 *
+	 * @param	bool	$persistent
+	 * @return	SQLite3
+	 */
+	public function db_connect($persistent = FALSE)
+	{
+		if ($persistent)
+		{
+			log_message('debug', 'SQLite3 doesn\'t support persistent connections');
+		}
+
+		try
+		{
+			return ( ! $this->password)
+				? new SQLite3($this->database)
+				: new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
+		}
+		catch (Exception $e)
+		{
+			return FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database version number
+	 *
+	 * @return	string
+	 */
+	public function version()
+	{
+		if (isset($this->data_cache['version']))
+		{
+			return $this->data_cache['version'];
+		}
+
+		$version = SQLite3::version();
+		return $this->data_cache['version'] = $version['versionString'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Execute the query
+	 *
+	 * @todo	Implement use of SQLite3::querySingle(), if needed
+	 * @param	string	$sql
+	 * @return	mixed	SQLite3Result object or bool
+	 */
+	protected function _execute($sql)
+	{
+		return $this->is_write_type($sql)
+			? $this->conn_id->exec($sql)
+			: $this->conn_id->query($sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Begin Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_begin()
+	{
+		return $this->conn_id->exec('BEGIN TRANSACTION');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Commit Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_commit()
+	{
+		return $this->conn_id->exec('END TRANSACTION');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rollback Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_rollback()
+	{
+		return $this->conn_id->exec('ROLLBACK');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Platform-dependent string escape
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	protected function _escape_str($str)
+	{
+		return $this->conn_id->escapeString($str);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Affected Rows
+	 *
+	 * @return	int
+	 */
+	public function affected_rows()
+	{
+		return $this->conn_id->changes();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert ID
+	 *
+	 * @return	int
+	 */
+	public function insert_id()
+	{
+		return $this->conn_id->lastInsertRowID();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Show table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool	$prefix_limit
+	 * @return	string
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
+			.(($prefix_limit !== FALSE && $this->dbprefix != '')
+				? ' AND "NAME" LIKE \''.$this->escape_like_str($this->dbprefix).'%\' '.sprintf($this->_like_escape_str, $this->_like_escape_chr)
+				: '');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * @param	string	$table	Table name
+	 * @return	array
+	 */
+	public function list_fields($table)
+	{
+		if (($result = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE)
+		{
+			return FALSE;
+		}
+
+		$fields = array();
+		foreach ($result->result_array() as $row)
+		{
+			$fields[] = $row['name'];
+		}
+
+		return $fields;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		if (($query = $this->query('PRAGMA TABLE_INFO('.$this->protect_identifiers($table, TRUE, NULL, FALSE).')')) === FALSE)
+		{
+			return FALSE;
+		}
+
+		$query = $query->result_array();
+		if (empty($query))
+		{
+			return FALSE;
+		}
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]['name'];
+			$retval[$i]->type		= $query[$i]['type'];
+			$retval[$i]->max_length		= NULL;
+			$retval[$i]->default		= $query[$i]['dflt_value'];
+			$retval[$i]->primary_key	= isset($query[$i]['pk']) ? (int) $query[$i]['pk'] : 0;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error
+	 *
+	 * Returns an array containing code and message of the last
+	 * database error that has occurred.
+	 *
+	 * @return	array
+	 */
+	public function error()
+	{
+		return array('code' => $this->conn_id->lastErrorCode(), 'message' => $this->conn_id->lastErrorMsg());
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Replace statement
+	 *
+	 * Generates a platform-specific replace string from the supplied data
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$keys	INSERT keys
+	 * @param	array	$values	INSERT values
+	 * @return	string
+	 */
+	protected function _replace($table, $keys, $values)
+	{
+		return 'INSERT OR '.parent::_replace($table, $keys, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Truncate statement
+	 *
+	 * Generates a platform-specific truncate string from the supplied data
+	 *
+	 * If the database does not support the TRUNCATE statement,
+	 * then this method maps to 'DELETE FROM table'
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _truncate($table)
+	{
+		return 'DELETE FROM '.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close DB Connection
+	 *
+	 * @return	void
+	 */
+	protected function _close()
+	{
+		$this->conn_id->close();
+	}
+
+}
diff --git a/system/database/drivers/sqlite3/sqlite3_forge.php b/system/database/drivers/sqlite3/sqlite3_forge.php
new file mode 100644
index 0000000..5658b3e
--- /dev/null
+++ b/system/database/drivers/sqlite3/sqlite3_forge.php
@@ -0,0 +1,226 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * SQLite3 Forge Class
+ *
+ * @category	Database
+ * @author	Andrey Andreev
+ * @link	https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_sqlite3_forge extends CI_DB_forge {
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	bool|array
+	 */
+	protected $_unsigned		= FALSE;
+
+	/**
+	 * NULL value representation in CREATE/ALTER TABLE statements
+	 *
+	 * @var	string
+	 */
+	protected $_null		= 'NULL';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	object	&$db	Database object
+	 * @return	void
+	 */
+	public function __construct(&$db)
+	{
+		parent::__construct($db);
+
+		if (version_compare($this->db->version(), '3.3', '<'))
+		{
+			$this->_create_table_if = FALSE;
+			$this->_drop_table_if   = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Create database
+	 *
+	 * @param	string	$db_name
+	 * @return	bool
+	 */
+	public function create_database($db_name)
+	{
+		// In SQLite, a database is created when you connect to the database.
+		// We'll return TRUE so that an error isn't generated
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Drop database
+	 *
+	 * @param	string	$db_name	(ignored)
+	 * @return	bool
+	 */
+	public function drop_database($db_name)
+	{
+		// In SQLite, a database is dropped when we delete a file
+		if (file_exists($this->db->database))
+		{
+			// We need to close the pseudo-connection first
+			$this->db->close();
+			if ( ! @unlink($this->db->database))
+			{
+				return $this->db->db_debug ? $this->db->display_error('db_unable_to_drop') : FALSE;
+			}
+			elseif ( ! empty($this->db->data_cache['db_names']))
+			{
+				$key = array_search(strtolower($this->db->database), array_map('strtolower', $this->db->data_cache['db_names']), TRUE);
+				if ($key !== FALSE)
+				{
+					unset($this->db->data_cache['db_names'][$key]);
+				}
+			}
+
+			return TRUE;
+		}
+
+		return $this->db->db_debug ? $this->db->display_error('db_unable_to_drop') : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @todo	implement drop_column(), modify_column()
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if ($alter_type === 'DROP' OR $alter_type === 'CHANGE')
+		{
+			// drop_column():
+			//	BEGIN TRANSACTION;
+			//	CREATE TEMPORARY TABLE t1_backup(a,b);
+			//	INSERT INTO t1_backup SELECT a,b FROM t1;
+			//	DROP TABLE t1;
+			//	CREATE TABLE t1(a,b);
+			//	INSERT INTO t1 SELECT a,b FROM t1_backup;
+			//	DROP TABLE t1_backup;
+			//	COMMIT;
+
+			return FALSE;
+		}
+
+		return parent::_alter_table($alter_type, $table, $field);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Process column
+	 *
+	 * @param	array	$field
+	 * @return	string
+	 */
+	protected function _process_column($field)
+	{
+		return $this->db->escape_identifiers($field['name'])
+			.' '.$field['type']
+			.$field['auto_increment']
+			.$field['null']
+			.$field['unique']
+			.$field['default'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'ENUM':
+			case 'SET':
+				$attributes['TYPE'] = 'TEXT';
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE)
+		{
+			$field['type'] = 'INTEGER PRIMARY KEY';
+			$field['default'] = '';
+			$field['null'] = '';
+			$field['unique'] = '';
+			$field['auto_increment'] = ' AUTOINCREMENT';
+
+			$this->primary_keys = array();
+		}
+	}
+
+}
diff --git a/system/database/drivers/sqlite3/sqlite3_result.php b/system/database/drivers/sqlite3/sqlite3_result.php
new file mode 100644
index 0000000..47fe9d2
--- /dev/null
+++ b/system/database/drivers/sqlite3/sqlite3_result.php
@@ -0,0 +1,195 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * SQLite3 Result Class
+ *
+ * This class extends the parent result class: CI_DB_result
+ *
+ * @category	Database
+ * @author		Andrey Andreev
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_sqlite3_result extends CI_DB_result {
+
+	/**
+	 * Number of fields in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_fields()
+	{
+		return $this->result_id->numColumns();
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * Generates an array of column names
+	 *
+	 * @return	array
+	 */
+	public function list_fields()
+	{
+		$field_names = array();
+		for ($i = 0, $c = $this->num_fields(); $i < $c; $i++)
+		{
+			$field_names[] = $this->result_id->columnName($i);
+		}
+
+		return $field_names;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data
+	 *
+	 * Generates an array of objects containing field meta-data
+	 *
+	 * @return	array
+	 */
+	public function field_data()
+	{
+		static $data_types = array(
+			SQLITE3_INTEGER	=> 'integer',
+			SQLITE3_FLOAT	=> 'float',
+			SQLITE3_TEXT	=> 'text',
+			SQLITE3_BLOB	=> 'blob',
+			SQLITE3_NULL	=> 'null'
+		);
+
+		$retval = array();
+		for ($i = 0, $c = $this->num_fields(); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $this->result_id->columnName($i);
+
+			$type = $this->result_id->columnType($i);
+			$retval[$i]->type		= isset($data_types[$type]) ? $data_types[$type] : $type;
+
+			$retval[$i]->max_length		= NULL;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Free the result
+	 *
+	 * @return	void
+	 */
+	public function free_result()
+	{
+		if (is_object($this->result_id))
+		{
+			$this->result_id->finalize();
+			$this->result_id = NULL;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - associative array
+	 *
+	 * Returns the result set as an array
+	 *
+	 * @return	array
+	 */
+	protected function _fetch_assoc()
+	{
+		return $this->result_id->fetchArray(SQLITE3_ASSOC);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - object
+	 *
+	 * Returns the result set as an object
+	 *
+	 * @param	string	$class_name
+	 * @return	object
+	 */
+	protected function _fetch_object($class_name = 'stdClass')
+	{
+		// No native support for fetching rows as objects
+		if (($row = $this->result_id->fetchArray(SQLITE3_ASSOC)) === FALSE)
+		{
+			return FALSE;
+		}
+		elseif ($class_name === 'stdClass')
+		{
+			return (object) $row;
+		}
+
+		$class_name = new $class_name();
+		foreach (array_keys($row) as $key)
+		{
+			$class_name->$key = $row[$key];
+		}
+
+		return $class_name;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Data Seek
+	 *
+	 * Moves the internal pointer to the desired offset. We call
+	 * this internally before fetching results to make sure the
+	 * result set starts at zero.
+	 *
+	 * @param	int	$n	(ignored)
+	 * @return	array
+	 */
+	public function data_seek($n = 0)
+	{
+		// Only resetting to the start of the result set is supported
+		return ($n > 0) ? FALSE : $this->result_id->reset();
+	}
+
+}
diff --git a/system/database/drivers/sqlite3/sqlite3_utility.php b/system/database/drivers/sqlite3/sqlite3_utility.php
new file mode 100644
index 0000000..90316bc
--- /dev/null
+++ b/system/database/drivers/sqlite3/sqlite3_utility.php
@@ -0,0 +1,62 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * SQLite3 Utility Class
+ *
+ * @category	Database
+ * @author	Andrey Andreev
+ * @link	https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_sqlite3_utility extends CI_DB_utility {
+
+	/**
+	 * Export
+	 *
+	 * @param	array	$params	Preferences
+	 * @return	mixed
+	 */
+	protected function _backup($params = array())
+	{
+		// Not supported
+		return $this->db->display_error('db_unsupported_feature');
+	}
+
+}
diff --git a/system/database/drivers/sqlsrv/index.html b/system/database/drivers/sqlsrv/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/drivers/sqlsrv/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/system/database/drivers/sqlsrv/sqlsrv_driver.php b/system/database/drivers/sqlsrv/sqlsrv_driver.php
new file mode 100644
index 0000000..7877794
--- /dev/null
+++ b/system/database/drivers/sqlsrv/sqlsrv_driver.php
@@ -0,0 +1,544 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 2.0.3
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * SQLSRV Database Adapter Class
+ *
+ * Note: _DB is an extender class that the app controller
+ * creates dynamically based on whether the query builder
+ * class is being used or not.
+ *
+ * @package		CodeIgniter
+ * @subpackage	Drivers
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_sqlsrv_driver extends CI_DB {
+
+	/**
+	 * Database driver
+	 *
+	 * @var	string
+	 */
+	public $dbdriver = 'sqlsrv';
+
+	/**
+	 * Scrollable flag
+	 *
+	 * Determines what cursor type to use when executing queries.
+	 *
+	 * FALSE or SQLSRV_CURSOR_FORWARD would increase performance,
+	 * but would disable num_rows() (and possibly insert_id())
+	 *
+	 * @var	mixed
+	 */
+	public $scrollable;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ORDER BY random keyword
+	 *
+	 * @var	array
+	 */
+	protected $_random_keyword = array('NEWID()', 'RAND(%d)');
+
+	/**
+	 * Quoted identifier flag
+	 *
+	 * Whether to use SQL-92 standard quoted identifier
+	 * (double quotes) or brackets for identifier escaping.
+	 *
+	 * @var	bool
+	 */
+	protected $_quoted_identifier = TRUE;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params
+	 * @return	void
+	 */
+	public function __construct($params)
+	{
+		parent::__construct($params);
+
+		// This is only supported as of SQLSRV 3.0
+		if ($this->scrollable === NULL)
+		{
+			$this->scrollable = defined('SQLSRV_CURSOR_CLIENT_BUFFERED')
+				? SQLSRV_CURSOR_CLIENT_BUFFERED
+				: FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database connection
+	 *
+	 * @param	bool	$pooling
+	 * @return	resource
+	 */
+	public function db_connect($pooling = FALSE)
+	{
+		$charset = in_array(strtolower($this->char_set), array('utf-8', 'utf8'), TRUE)
+			? 'UTF-8' : SQLSRV_ENC_CHAR;
+
+		$connection = array(
+			'UID'			=> empty($this->username) ? '' : $this->username,
+			'PWD'			=> empty($this->password) ? '' : $this->password,
+			'Database'		=> $this->database,
+			'ConnectionPooling'	=> ($pooling === TRUE) ? 1 : 0,
+			'CharacterSet'		=> $charset,
+			'Encrypt'		=> ($this->encrypt === TRUE) ? 1 : 0,
+			'ReturnDatesAsStrings'	=> 1
+		);
+
+		// If the username and password are both empty, assume this is a
+		// 'Windows Authentication Mode' connection.
+		if (empty($connection['UID']) && empty($connection['PWD']))
+		{
+			unset($connection['UID'], $connection['PWD']);
+		}
+
+		if (FALSE !== ($this->conn_id = sqlsrv_connect($this->hostname, $connection)))
+		{
+			// Determine how identifiers are escaped
+			$query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi');
+			$query = $query->row_array();
+			$this->_quoted_identifier = empty($query) ? FALSE : (bool) $query['qi'];
+			$this->_escape_char = ($this->_quoted_identifier) ? '"' : array('[', ']');
+		}
+
+		return $this->conn_id;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Select the database
+	 *
+	 * @param	string	$database
+	 * @return	bool
+	 */
+	public function db_select($database = '')
+	{
+		if ($database === '')
+		{
+			$database = $this->database;
+		}
+
+		if ($this->_execute('USE '.$this->escape_identifiers($database)))
+		{
+			$this->database = $database;
+			$this->data_cache = array();
+			return TRUE;
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Execute the query
+	 *
+	 * @param	string	$sql	an SQL query
+	 * @return	resource
+	 */
+	protected function _execute($sql)
+	{
+		return ($this->scrollable === FALSE OR $this->is_write_type($sql))
+			? sqlsrv_query($this->conn_id, $sql)
+			: sqlsrv_query($this->conn_id, $sql, NULL, array('Scrollable' => $this->scrollable));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Begin Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_begin()
+	{
+		return sqlsrv_begin_transaction($this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Commit Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_commit()
+	{
+		return sqlsrv_commit($this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Rollback Transaction
+	 *
+	 * @return	bool
+	 */
+	protected function _trans_rollback()
+	{
+		return sqlsrv_rollback($this->conn_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Affected Rows
+	 *
+	 * @return	int
+	 */
+	public function affected_rows()
+	{
+		return sqlsrv_rows_affected($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert ID
+	 *
+	 * Returns the last id created in the Identity column.
+	 *
+	 * @return	string
+	 */
+	public function insert_id()
+	{
+		return $this->query('SELECT SCOPE_IDENTITY() AS insert_id')->row()->insert_id;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Database version number
+	 *
+	 * @return	string
+	 */
+	public function version()
+	{
+		if (isset($this->data_cache['version']))
+		{
+			return $this->data_cache['version'];
+		}
+
+		if ( ! $this->conn_id OR ($info = sqlsrv_server_info($this->conn_id)) === FALSE)
+		{
+			return FALSE;
+		}
+
+		return $this->data_cache['version'] = $info['SQLServerVersion'];
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * List table query
+	 *
+	 * Generates a platform-specific query string so that the table names can be fetched
+	 *
+	 * @param	bool
+	 * @return	string	$prefix_limit
+	 */
+	protected function _list_tables($prefix_limit = FALSE)
+	{
+		$sql = 'SELECT '.$this->escape_identifiers('name')
+			.' FROM '.$this->escape_identifiers('sysobjects')
+			.' WHERE '.$this->escape_identifiers('type')." = 'U'";
+
+		if ($prefix_limit === TRUE && $this->dbprefix !== '')
+		{
+			$sql .= ' AND '.$this->escape_identifiers('name')." LIKE '".$this->escape_like_str($this->dbprefix)."%' "
+				.sprintf($this->_escape_like_str, $this->_escape_like_chr);
+		}
+
+		return $sql.' ORDER BY '.$this->escape_identifiers('name');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * List column query
+	 *
+	 * Generates a platform-specific query string so that the column names can be fetched
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _list_columns($table = '')
+	{
+		return 'SELECT COLUMN_NAME
+			FROM INFORMATION_SCHEMA.Columns
+			WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Returns an object with field data
+	 *
+	 * @param	string	$table
+	 * @return	array
+	 */
+	public function field_data($table)
+	{
+		$sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT
+			FROM INFORMATION_SCHEMA.Columns
+			WHERE UPPER(TABLE_NAME) = '.$this->escape(strtoupper($table));
+
+		if (($query = $this->query($sql)) === FALSE)
+		{
+			return FALSE;
+		}
+		$query = $query->result_object();
+
+		$retval = array();
+		for ($i = 0, $c = count($query); $i < $c; $i++)
+		{
+			$retval[$i]			= new stdClass();
+			$retval[$i]->name		= $query[$i]->COLUMN_NAME;
+			$retval[$i]->type		= $query[$i]->DATA_TYPE;
+			$retval[$i]->max_length		= ($query[$i]->CHARACTER_MAXIMUM_LENGTH > 0) ? $query[$i]->CHARACTER_MAXIMUM_LENGTH : $query[$i]->NUMERIC_PRECISION;
+			$retval[$i]->default		= $query[$i]->COLUMN_DEFAULT;
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Error
+	 *
+	 * Returns an array containing code and message of the last
+	 * database error that has occurred.
+	 *
+	 * @return	array
+	 */
+	public function error()
+	{
+		$error = array('code' => '00000', 'message' => '');
+		$sqlsrv_errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
+
+		if ( ! is_array($sqlsrv_errors))
+		{
+			return $error;
+		}
+
+		$sqlsrv_error = array_shift($sqlsrv_errors);
+		if (isset($sqlsrv_error['SQLSTATE']))
+		{
+			$error['code'] = isset($sqlsrv_error['code']) ? $sqlsrv_error['SQLSTATE'].'/'.$sqlsrv_error['code'] : $sqlsrv_error['SQLSTATE'];
+		}
+		elseif (isset($sqlsrv_error['code']))
+		{
+			$error['code'] = $sqlsrv_error['code'];
+		}
+
+		if (isset($sqlsrv_error['message']))
+		{
+			$error['message'] = $sqlsrv_error['message'];
+		}
+
+		return $error;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update statement
+	 *
+	 * Generates a platform-specific update string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @param	array	$values
+	 * @return	string
+	 */
+	protected function _update($table, $values)
+	{
+		$this->qb_limit = FALSE;
+		$this->qb_orderby = array();
+		return parent::_update($table, $values);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Truncate statement
+	 *
+	 * Generates a platform-specific truncate string from the supplied data
+	 *
+	 * If the database does not support the TRUNCATE statement,
+	 * then this method maps to 'DELETE FROM table'
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _truncate($table)
+	{
+		return 'TRUNCATE TABLE '.$table;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Delete statement
+	 *
+	 * Generates a platform-specific delete string from the supplied data
+	 *
+	 * @param	string	$table
+	 * @return	string
+	 */
+	protected function _delete($table)
+	{
+		if ($this->qb_limit)
+		{
+			return 'WITH ci_delete AS (SELECT TOP '.$this->qb_limit.' * FROM '.$table.$this->_compile_wh('qb_where').') DELETE FROM ci_delete';
+		}
+
+		return parent::_delete($table);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * LIMIT
+	 *
+	 * Generates a platform-specific LIMIT clause
+	 *
+	 * @param	string	$sql	SQL Query
+	 * @return	string
+	 */
+	protected function _limit($sql)
+	{
+		// As of SQL Server 2012 (11.0.*) OFFSET is supported
+		if (version_compare($this->version(), '11', '>='))
+		{
+			// SQL Server OFFSET-FETCH can be used only with the ORDER BY clause
+			empty($this->qb_orderby) && $sql .= ' ORDER BY 1';
+
+			return $sql.' OFFSET '.(int) $this->qb_offset.' ROWS FETCH NEXT '.$this->qb_limit.' ROWS ONLY';
+		}
+
+		$limit = $this->qb_offset + $this->qb_limit;
+
+		// An ORDER BY clause is required for ROW_NUMBER() to work
+		if ($this->qb_offset && ! empty($this->qb_orderby))
+		{
+			$orderby = $this->_compile_order_by();
+
+			// We have to strip the ORDER BY clause
+			$sql = trim(substr($sql, 0, strrpos($sql, $orderby)));
+
+			// Get the fields to select from our subquery, so that we can avoid CI_rownum appearing in the actual results
+			if (count($this->qb_select) === 0 OR strpos(implode(',', $this->qb_select), '*') !== FALSE)
+			{
+				$select = '*'; // Inevitable
+			}
+			else
+			{
+				// Use only field names and their aliases, everything else is out of our scope.
+				$select = array();
+				$field_regexp = ($this->_quoted_identifier)
+					? '("[^\"]+")' : '(\[[^\]]+\])';
+				for ($i = 0, $c = count($this->qb_select); $i < $c; $i++)
+				{
+					$select[] = preg_match('/(?:\s|\.)'.$field_regexp.'$/i', $this->qb_select[$i], $m)
+						? $m[1] : $this->qb_select[$i];
+				}
+				$select = implode(', ', $select);
+			}
+
+			return 'SELECT '.$select." FROM (\n\n"
+				.preg_replace('/^(SELECT( DISTINCT)?)/i', '\\1 ROW_NUMBER() OVER('.trim($orderby).') AS '.$this->escape_identifiers('CI_rownum').', ', $sql)
+				."\n\n) ".$this->escape_identifiers('CI_subquery')
+				."\nWHERE ".$this->escape_identifiers('CI_rownum').' BETWEEN '.($this->qb_offset + 1).' AND '.$limit;
+		}
+
+		return preg_replace('/(^\SELECT (DISTINCT)?)/i','\\1 TOP '.$limit.' ', $sql);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Insert batch statement
+	 *
+	 * Generates a platform-specific insert string from the supplied data.
+	 *
+	 * @param	string	$table	Table name
+	 * @param	array	$keys	INSERT keys
+	 * @param	array	$values	INSERT values
+	 * @return	string|bool
+	 */
+	protected function _insert_batch($table, $keys, $values)
+	{
+		// Multiple-value inserts are only supported as of SQL Server 2008
+		if (version_compare($this->version(), '10', '>='))
+		{
+			return parent::_insert_batch($table, $keys, $values);
+		}
+
+		return ($this->db_debug) ? $this->display_error('db_unsupported_feature') : FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Close DB Connection
+	 *
+	 * @return	void
+	 */
+	protected function _close()
+	{
+		sqlsrv_close($this->conn_id);
+	}
+
+}
diff --git a/system/database/drivers/sqlsrv/sqlsrv_forge.php b/system/database/drivers/sqlsrv/sqlsrv_forge.php
new file mode 100644
index 0000000..dca7f75
--- /dev/null
+++ b/system/database/drivers/sqlsrv/sqlsrv_forge.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 2.0.3
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * SQLSRV Forge Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_sqlsrv_forge extends CI_DB_forge {
+
+	/**
+	 * CREATE TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_create_table_if	= "IF NOT EXISTS (SELECT * FROM sysobjects WHERE ID = object_id(N'%s') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)\nCREATE TABLE";
+
+	/**
+	 * DROP TABLE IF statement
+	 *
+	 * @var	string
+	 */
+	protected $_drop_table_if	= "IF EXISTS (SELECT * FROM sysobjects WHERE ID = object_id(N'%s') AND OBJECTPROPERTY(id, N'IsUserTable') = 1)\nDROP TABLE";
+
+	/**
+	 * UNSIGNED support
+	 *
+	 * @var	array
+	 */
+	protected $_unsigned		= array(
+		'TINYINT'	=> 'SMALLINT',
+		'SMALLINT'	=> 'INT',
+		'INT'		=> 'BIGINT',
+		'REAL'		=> 'FLOAT'
+	);
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * ALTER TABLE
+	 *
+	 * @param	string	$alter_type	ALTER type
+	 * @param	string	$table		Table name
+	 * @param	mixed	$field		Column definition
+	 * @return	string|string[]
+	 */
+	protected function _alter_table($alter_type, $table, $field)
+	{
+		if (in_array($alter_type, array('ADD', 'DROP'), TRUE))
+		{
+			return parent::_alter_table($alter_type, $table, $field);
+		}
+
+		$sql = 'ALTER TABLE '.$this->db->escape_identifiers($table).' ALTER COLUMN ';
+		$sqls = array();
+		for ($i = 0, $c = count($field); $i < $c; $i++)
+		{
+			$sqls[] = $sql.$this->_process_column($field[$i]);
+		}
+
+		return $sqls;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute TYPE
+	 *
+	 * Performs a data type mapping between different databases.
+	 *
+	 * @param	array	&$attributes
+	 * @return	void
+	 */
+	protected function _attr_type(&$attributes)
+	{
+		if (isset($attributes['CONSTRAINT']) && strpos($attributes['TYPE'], 'INT') !== FALSE)
+		{
+			unset($attributes['CONSTRAINT']);
+		}
+
+		switch (strtoupper($attributes['TYPE']))
+		{
+			case 'MEDIUMINT':
+				$attributes['TYPE'] = 'INTEGER';
+				$attributes['UNSIGNED'] = FALSE;
+				return;
+			case 'INTEGER':
+				$attributes['TYPE'] = 'INT';
+				return;
+			default: return;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field attribute AUTO_INCREMENT
+	 *
+	 * @param	array	&$attributes
+	 * @param	array	&$field
+	 * @return	void
+	 */
+	protected function _attr_auto_increment(&$attributes, &$field)
+	{
+		if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === TRUE && stripos($field['type'], 'int') !== FALSE)
+		{
+			$field['auto_increment'] = ' IDENTITY(1,1)';
+		}
+	}
+
+}
diff --git a/system/database/drivers/sqlsrv/sqlsrv_result.php b/system/database/drivers/sqlsrv/sqlsrv_result.php
new file mode 100644
index 0000000..a3a582b
--- /dev/null
+++ b/system/database/drivers/sqlsrv/sqlsrv_result.php
@@ -0,0 +1,194 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 2.0.3
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * SQLSRV Result Class
+ *
+ * This class extends the parent result class: CI_DB_result
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_sqlsrv_result extends CI_DB_result {
+
+	/**
+	 * Scrollable flag
+	 *
+	 * @var	mixed
+	 */
+	public $scrollable;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Constructor
+	 *
+	 * @param	object	$driver_object
+	 * @return	void
+	 */
+	public function __construct(&$driver_object)
+	{
+		parent::__construct($driver_object);
+
+		$this->scrollable = $driver_object->scrollable;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Number of rows in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_rows()
+	{
+		// sqlsrv_num_rows() doesn't work with the FORWARD and DYNAMIC cursors (FALSE is the same as FORWARD)
+		if ( ! in_array($this->scrollable, array(FALSE, SQLSRV_CURSOR_FORWARD, SQLSRV_CURSOR_DYNAMIC), TRUE))
+		{
+			return parent::num_rows();
+		}
+
+		return is_int($this->num_rows)
+			? $this->num_rows
+			: $this->num_rows = sqlsrv_num_rows($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Number of fields in the result set
+	 *
+	 * @return	int
+	 */
+	public function num_fields()
+	{
+		return @sqlsrv_num_fields($this->result_id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Fetch Field Names
+	 *
+	 * Generates an array of column names
+	 *
+	 * @return	array
+	 */
+	public function list_fields()
+	{
+		$field_names = array();
+		foreach (sqlsrv_field_metadata($this->result_id) as $offset => $field)
+		{
+			$field_names[] = $field['Name'];
+		}
+
+		return $field_names;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Field data
+	 *
+	 * Generates an array of objects containing field meta-data
+	 *
+	 * @return	array
+	 */
+	public function field_data()
+	{
+		$retval = array();
+		foreach (sqlsrv_field_metadata($this->result_id) as $i => $field)
+		{
+			$retval[$i]		= new stdClass();
+			$retval[$i]->name	= $field['Name'];
+			$retval[$i]->type	= $field['Type'];
+			$retval[$i]->max_length	= $field['Size'];
+		}
+
+		return $retval;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Free the result
+	 *
+	 * @return	void
+	 */
+	public function free_result()
+	{
+		if (is_resource($this->result_id))
+		{
+			sqlsrv_free_stmt($this->result_id);
+			$this->result_id = FALSE;
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - associative array
+	 *
+	 * Returns the result set as an array
+	 *
+	 * @return	array
+	 */
+	protected function _fetch_assoc()
+	{
+		return sqlsrv_fetch_array($this->result_id, SQLSRV_FETCH_ASSOC);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Result - object
+	 *
+	 * Returns the result set as an object
+	 *
+	 * @param	string	$class_name
+	 * @return	object
+	 */
+	protected function _fetch_object($class_name = 'stdClass')
+	{
+		return sqlsrv_fetch_object($this->result_id, $class_name);
+	}
+
+}
diff --git a/system/database/drivers/sqlsrv/sqlsrv_utility.php b/system/database/drivers/sqlsrv/sqlsrv_utility.php
new file mode 100644
index 0000000..e51bc72
--- /dev/null
+++ b/system/database/drivers/sqlsrv/sqlsrv_utility.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 2.0.3
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * SQLSRV Utility Class
+ *
+ * @category	Database
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/database/
+ */
+class CI_DB_sqlsrv_utility extends CI_DB_utility {
+
+	/**
+	 * List databases statement
+	 *
+	 * @var	string
+	 */
+	protected $_list_databases	= 'EXEC sp_helpdb'; // Can also be: EXEC sp_databases
+
+	/**
+	 * OPTIMIZE TABLE statement
+	 *
+	 * @var	string
+	 */
+	protected $_optimize_table	= 'ALTER INDEX all ON %s REORGANIZE';
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Export
+	 *
+	 * @param	array	$params	Preferences
+	 * @return	bool
+	 */
+	protected function _backup($params = array())
+	{
+		// Currently unsupported
+		return $this->db->display_error('db_unsupported_feature');
+	}
+
+}
diff --git a/system/database/index.html b/system/database/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/database/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/system/fonts/index.html b/system/fonts/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/fonts/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/system/fonts/texb.ttf b/system/fonts/texb.ttf
new file mode 100644
index 0000000..383c88b
Binary files /dev/null and b/system/fonts/texb.ttf differ
diff --git a/system/helpers/array_helper.php b/system/helpers/array_helper.php
new file mode 100644
index 0000000..0617fde
--- /dev/null
+++ b/system/helpers/array_helper.php
@@ -0,0 +1,116 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Array Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/array_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('element'))
+{
+	/**
+	 * Element
+	 *
+	 * Lets you determine whether an array index is set and whether it has a value.
+	 * If the element is empty it returns NULL (or whatever you specify as the default value.)
+	 *
+	 * @param	string
+	 * @param	array
+	 * @param	mixed
+	 * @return	mixed	depends on what the array contains
+	 */
+	function element($item, array $array, $default = NULL)
+	{
+		return array_key_exists($item, $array) ? $array[$item] : $default;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('random_element'))
+{
+	/**
+	 * Random Element - Takes an array as input and returns a random element
+	 *
+	 * @param	array
+	 * @return	mixed	depends on what the array contains
+	 */
+	function random_element($array)
+	{
+		return is_array($array) ? $array[array_rand($array)] : $array;
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('elements'))
+{
+	/**
+	 * Elements
+	 *
+	 * Returns only the array items specified. Will return a default value if
+	 * it is not set.
+	 *
+	 * @param	array
+	 * @param	array
+	 * @param	mixed
+	 * @return	mixed	depends on what the array contains
+	 */
+	function elements($items, array $array, $default = NULL)
+	{
+		$return = array();
+
+		is_array($items) OR $items = array($items);
+
+		foreach ($items as $item)
+		{
+			$return[$item] = array_key_exists($item, $array) ? $array[$item] : $default;
+		}
+
+		return $return;
+	}
+}
diff --git a/system/helpers/captcha_helper.php b/system/helpers/captcha_helper.php
new file mode 100644
index 0000000..9fcbd1b
--- /dev/null
+++ b/system/helpers/captcha_helper.php
@@ -0,0 +1,353 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter CAPTCHA Helper
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/captcha_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('create_captcha'))
+{
+	/**
+	 * Create CAPTCHA
+	 *
+	 * @param	array	$data		Data for the CAPTCHA
+	 * @param	string	$img_path	Path to create the image in (deprecated)
+	 * @param	string	$img_url	URL to the CAPTCHA image folder (deprecated)
+	 * @param	string	$font_path	Server path to font (deprecated)
+	 * @return	string
+	 */
+	function create_captcha($data = '', $img_path = '', $img_url = '', $font_path = '')
+	{
+		$defaults = array(
+			'word'		=> '',
+			'img_path'	=> '',
+			'img_url'	=> '',
+			'img_width'	=> '150',
+			'img_height'	=> '30',
+			'font_path'	=> '',
+			'expiration'	=> 7200,
+			'word_length'	=> 8,
+			'font_size'	=> 16,
+			'img_id'	=> '',
+			'pool'		=> '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
+			'colors'	=> array(
+				'background'	=> array(255,255,255),
+				'border'	=> array(153,102,102),
+				'text'		=> array(204,153,153),
+				'grid'		=> array(255,182,182)
+			)
+		);
+
+		foreach ($defaults as $key => $val)
+		{
+			if ( ! is_array($data) && empty($$key))
+			{
+				$$key = $val;
+			}
+			else
+			{
+				$$key = isset($data[$key]) ? $data[$key] : $val;
+			}
+		}
+
+		if ( ! extension_loaded('gd'))
+		{
+			log_message('error', 'create_captcha(): GD extension is not loaded.');
+			return FALSE;
+		}
+
+		if ($img_path === '' OR $img_url === '')
+		{
+			log_message('error', 'create_captcha(): $img_path and $img_url are required.');
+			return FALSE;
+		}
+
+		if ( ! is_dir($img_path) OR ! is_really_writable($img_path))
+		{
+			log_message('error', "create_captcha(): '{$img_path}' is not a dir, nor is it writable.");
+			return FALSE;
+		}
+
+		// -----------------------------------
+		// Remove old images
+		// -----------------------------------
+
+		$now = microtime(TRUE);
+
+		$current_dir = @opendir($img_path);
+		while ($filename = @readdir($current_dir))
+		{
+			if (in_array(substr($filename, -4), array('.jpg', '.png'))
+				&& (str_replace(array('.jpg', '.png'), '', $filename) + $expiration) < $now)
+			{
+				@unlink($img_path.$filename);
+			}
+		}
+
+		@closedir($current_dir);
+
+		// -----------------------------------
+		// Do we have a "word" yet?
+		// -----------------------------------
+
+		if (empty($word))
+		{
+			$word = '';
+			$pool_length = strlen($pool);
+			$rand_max = $pool_length - 1;
+
+			// PHP7 or a suitable polyfill
+			if (function_exists('random_int'))
+			{
+				try
+				{
+					for ($i = 0; $i < $word_length; $i++)
+					{
+						$word .= $pool[random_int(0, $rand_max)];
+					}
+				}
+				catch (Exception $e)
+				{
+					// This means fallback to the next possible
+					// alternative to random_int()
+					$word = '';
+				}
+			}
+		}
+
+		if (empty($word))
+		{
+			// Nobody will have a larger character pool than
+			// 256 characters, but let's handle it just in case ...
+			//
+			// No, I do not care that the fallback to mt_rand() can
+			// handle it; if you trigger this, you're very obviously
+			// trying to break it. -- Narf
+			if ($pool_length > 256)
+			{
+				return FALSE;
+			}
+
+			// We'll try using the operating system's PRNG first,
+			// which we can access through CI_Security::get_random_bytes()
+			$security = get_instance()->security;
+
+			// To avoid numerous get_random_bytes() calls, we'll
+			// just try fetching as much bytes as we need at once.
+			if (($bytes = $security->get_random_bytes($pool_length)) !== FALSE)
+			{
+				$byte_index = $word_index = 0;
+				while ($word_index < $word_length)
+				{
+					// Do we have more random data to use?
+					// It could be exhausted by previous iterations
+					// ignoring bytes higher than $rand_max.
+					if ($byte_index === $pool_length)
+					{
+						// No failures should be possible if the
+						// first get_random_bytes() call didn't
+						// return FALSE, but still ...
+						for ($i = 0; $i < 5; $i++)
+						{
+							if (($bytes = $security->get_random_bytes($pool_length)) === FALSE)
+							{
+								continue;
+							}
+
+							$byte_index = 0;
+							break;
+						}
+
+						if ($bytes === FALSE)
+						{
+							// Sadly, this means fallback to mt_rand()
+							$word = '';
+							break;
+						}
+					}
+
+					list(, $rand_index) = unpack('C', $bytes[$byte_index++]);
+					if ($rand_index > $rand_max)
+					{
+						continue;
+					}
+
+					$word .= $pool[$rand_index];
+					$word_index++;
+				}
+			}
+		}
+
+		if (empty($word))
+		{
+			for ($i = 0; $i < $word_length; $i++)
+			{
+				$word .= $pool[mt_rand(0, $rand_max)];
+			}
+		}
+		elseif ( ! is_string($word))
+		{
+			$word = (string) $word;
+		}
+
+		// -----------------------------------
+		// Determine angle and position
+		// -----------------------------------
+		$length	= strlen($word);
+		$angle	= ($length >= 6) ? mt_rand(-($length-6), ($length-6)) : 0;
+		$x_axis	= mt_rand(6, (360/$length)-16);
+		$y_axis = ($angle >= 0) ? mt_rand($img_height, $img_width) : mt_rand(6, $img_height);
+
+		// Create image
+		// PHP.net recommends imagecreatetruecolor(), but it isn't always available
+		$im = function_exists('imagecreatetruecolor')
+			? imagecreatetruecolor($img_width, $img_height)
+			: imagecreate($img_width, $img_height);
+
+		// -----------------------------------
+		//  Assign colors
+		// ----------------------------------
+
+		is_array($colors) OR $colors = $defaults['colors'];
+
+		foreach (array_keys($defaults['colors']) as $key)
+		{
+			// Check for a possible missing value
+			is_array($colors[$key]) OR $colors[$key] = $defaults['colors'][$key];
+			$colors[$key] = imagecolorallocate($im, $colors[$key][0], $colors[$key][1], $colors[$key][2]);
+		}
+
+		// Create the rectangle
+		ImageFilledRectangle($im, 0, 0, $img_width, $img_height, $colors['background']);
+
+		// -----------------------------------
+		//  Create the spiral pattern
+		// -----------------------------------
+		$theta		= 1;
+		$thetac		= 7;
+		$radius		= 16;
+		$circles	= 20;
+		$points		= 32;
+
+		for ($i = 0, $cp = ($circles * $points) - 1; $i < $cp; $i++)
+		{
+			$theta += $thetac;
+			$rad = $radius * ($i / $points);
+			$x = ($rad * cos($theta)) + $x_axis;
+			$y = ($rad * sin($theta)) + $y_axis;
+			$theta += $thetac;
+			$rad1 = $radius * (($i + 1) / $points);
+			$x1 = ($rad1 * cos($theta)) + $x_axis;
+			$y1 = ($rad1 * sin($theta)) + $y_axis;
+			imageline($im, $x, $y, $x1, $y1, $colors['grid']);
+			$theta -= $thetac;
+		}
+
+		// -----------------------------------
+		//  Write the text
+		// -----------------------------------
+
+		$use_font = ($font_path !== '' && file_exists($font_path) && function_exists('imagettftext'));
+		if ($use_font === FALSE)
+		{
+			($font_size > 5) && $font_size = 5;
+			$x = mt_rand(0, $img_width / ($length / 3));
+			$y = 0;
+		}
+		else
+		{
+			($font_size > 30) && $font_size = 30;
+			$x = mt_rand(0, $img_width / ($length / 1.5));
+			$y = $font_size + 2;
+		}
+
+		for ($i = 0; $i < $length; $i++)
+		{
+			if ($use_font === FALSE)
+			{
+				$y = mt_rand(0 , $img_height / 2);
+				imagestring($im, $font_size, $x, $y, $word[$i], $colors['text']);
+				$x += ($font_size * 2);
+			}
+			else
+			{
+				$y = mt_rand($img_height / 2, $img_height - 3);
+				imagettftext($im, $font_size, $angle, $x, $y, $colors['text'], $font_path, $word[$i]);
+				$x += $font_size;
+			}
+		}
+
+		// Create the border
+		imagerectangle($im, 0, 0, $img_width - 1, $img_height - 1, $colors['border']);
+
+		// -----------------------------------
+		//  Generate the image
+		// -----------------------------------
+		$img_url = rtrim($img_url, '/').'/';
+
+		if (function_exists('imagejpeg'))
+		{
+			$img_filename = $now.'.jpg';
+			imagejpeg($im, $img_path.$img_filename);
+		}
+		elseif (function_exists('imagepng'))
+		{
+			$img_filename = $now.'.png';
+			imagepng($im, $img_path.$img_filename);
+		}
+		else
+		{
+			return FALSE;
+		}
+
+		$img = '<img '.($img_id === '' ? '' : 'id="'.$img_id.'"').' src="'.$img_url.$img_filename.'" style="width: '.$img_width.'px; height: '.$img_height .'px; border: 0;" alt=" " />';
+		ImageDestroy($im);
+
+		return array('word' => $word, 'time' => $now, 'image' => $img, 'filename' => $img_filename);
+	}
+}
diff --git a/system/helpers/cookie_helper.php b/system/helpers/cookie_helper.php
new file mode 100644
index 0000000..abe492f
--- /dev/null
+++ b/system/helpers/cookie_helper.php
@@ -0,0 +1,114 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Cookie Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/cookie_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('set_cookie'))
+{
+	/**
+	 * Set cookie
+	 *
+	 * Accepts seven parameters, or you can submit an associative
+	 * array in the first parameter containing all the values.
+	 *
+	 * @param	mixed
+	 * @param	string	the value of the cookie
+	 * @param	string	the number of seconds until expiration
+	 * @param	string	the cookie domain.  Usually:  .yourdomain.com
+	 * @param	string	the cookie path
+	 * @param	string	the cookie prefix
+	 * @param	bool	true makes the cookie secure
+	 * @param	bool	true makes the cookie accessible via http(s) only (no javascript)
+	 * @return	void
+	 */
+	function set_cookie($name, $value = '', $expire = '', $domain = '', $path = '/', $prefix = '', $secure = NULL, $httponly = NULL)
+	{
+		// Set the config file options
+		get_instance()->input->set_cookie($name, $value, $expire, $domain, $path, $prefix, $secure, $httponly);
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('get_cookie'))
+{
+	/**
+	 * Fetch an item from the COOKIE array
+	 *
+	 * @param	string
+	 * @param	bool
+	 * @return	mixed
+	 */
+	function get_cookie($index, $xss_clean = NULL)
+	{
+		is_bool($xss_clean) OR $xss_clean = (config_item('global_xss_filtering') === TRUE);
+		$prefix = isset($_COOKIE[$index]) ? '' : config_item('cookie_prefix');
+		return get_instance()->input->cookie($prefix.$index, $xss_clean);
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('delete_cookie'))
+{
+	/**
+	 * Delete a COOKIE
+	 *
+	 * @param	mixed
+	 * @param	string	the cookie domain. Usually: .yourdomain.com
+	 * @param	string	the cookie path
+	 * @param	string	the cookie prefix
+	 * @return	void
+	 */
+	function delete_cookie($name, $domain = '', $path = '/', $prefix = '')
+	{
+		set_cookie($name, '', '', $domain, $path, $prefix);
+	}
+}
diff --git a/system/helpers/date_helper.php b/system/helpers/date_helper.php
new file mode 100644
index 0000000..5b2f3e0
--- /dev/null
+++ b/system/helpers/date_helper.php
@@ -0,0 +1,743 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Date Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/date_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('now'))
+{
+	/**
+	 * Get "now" time
+	 *
+	 * Returns time() based on the timezone parameter or on the
+	 * "time_reference" setting
+	 *
+	 * @param	string
+	 * @return	int
+	 */
+	function now($timezone = NULL)
+	{
+		if (empty($timezone))
+		{
+			$timezone = config_item('time_reference');
+		}
+
+		if ($timezone === 'local' OR $timezone === date_default_timezone_get())
+		{
+			return time();
+		}
+
+		$datetime = new DateTime('now', new DateTimeZone($timezone));
+		sscanf($datetime->format('j-n-Y G:i:s'), '%d-%d-%d %d:%d:%d', $day, $month, $year, $hour, $minute, $second);
+
+		return mktime($hour, $minute, $second, $month, $day, $year);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('mdate'))
+{
+	/**
+	 * Convert MySQL Style Datecodes
+	 *
+	 * This function is identical to PHPs date() function,
+	 * except that it allows date codes to be formatted using
+	 * the MySQL style, where each code letter is preceded
+	 * with a percent sign:  %Y %m %d etc...
+	 *
+	 * The benefit of doing dates this way is that you don't
+	 * have to worry about escaping your text letters that
+	 * match the date codes.
+	 *
+	 * @param	string
+	 * @param	int
+	 * @return	int
+	 */
+	function mdate($datestr = '', $time = '')
+	{
+		if ($datestr === '')
+		{
+			return '';
+		}
+		elseif (empty($time))
+		{
+			$time = now();
+		}
+
+		$datestr = str_replace(
+			'%\\',
+			'',
+			preg_replace('/([a-z]+?){1}/i', '\\\\\\1', $datestr)
+		);
+
+		return date($datestr, $time);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('standard_date'))
+{
+	/**
+	 * Standard Date
+	 *
+	 * Returns a date formatted according to the submitted standard.
+	 *
+	 * As of PHP 5.2, the DateTime extension provides constants that
+	 * serve for the exact same purpose and are used with date().
+	 *
+	 * @todo	Remove in version 3.1+.
+	 * @deprecated	3.0.0	Use PHP's native date() instead.
+	 * @link	https://www.php.net/manual/en/class.datetime.php#datetime.constants.types
+	 *
+	 * @example	date(DATE_RFC822, now()); // default
+	 * @example	date(DATE_W3C, $time); // a different format and time
+	 *
+	 * @param	string	$fmt = 'DATE_RFC822'	the chosen format
+	 * @param	int	$time = NULL		Unix timestamp
+	 * @return	string
+	 */
+	function standard_date($fmt = 'DATE_RFC822', $time = NULL)
+	{
+		if (empty($time))
+		{
+			$time = now();
+		}
+
+		// Procedural style pre-defined constants from the DateTime extension
+		if (strpos($fmt, 'DATE_') !== 0 OR defined($fmt) === FALSE)
+		{
+			return FALSE;
+		}
+
+		return date(constant($fmt), $time);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('timespan'))
+{
+	/**
+	 * Timespan
+	 *
+	 * Returns a span of seconds in this format:
+	 *	10 days 14 hours 36 minutes 47 seconds
+	 *
+	 * @param	int	a number of seconds
+	 * @param	int	Unix timestamp
+	 * @param	int	a number of display units
+	 * @return	string
+	 */
+	function timespan($seconds = 1, $time = '', $units = 7)
+	{
+		$CI =& get_instance();
+		$CI->lang->load('date');
+
+		is_numeric($seconds) OR $seconds = 1;
+		is_numeric($time) OR $time = time();
+		is_numeric($units) OR $units = 7;
+
+		$seconds = ($time <= $seconds) ? 1 : $time - $seconds;
+
+		$str = array();
+		$years = floor($seconds / 31557600);
+
+		if ($years > 0)
+		{
+			$str[] = $years.' '.$CI->lang->line($years > 1 ? 'date_years' : 'date_year');
+		}
+
+		$seconds -= $years * 31557600;
+		$months = floor($seconds / 2629743);
+
+		if (count($str) < $units && ($years > 0 OR $months > 0))
+		{
+			if ($months > 0)
+			{
+				$str[] = $months.' '.$CI->lang->line($months > 1 ? 'date_months' : 'date_month');
+			}
+
+			$seconds -= $months * 2629743;
+		}
+
+		$weeks = floor($seconds / 604800);
+
+		if (count($str) < $units && ($years > 0 OR $months > 0 OR $weeks > 0))
+		{
+			if ($weeks > 0)
+			{
+				$str[] = $weeks.' '.$CI->lang->line($weeks > 1 ? 'date_weeks' : 'date_week');
+			}
+
+			$seconds -= $weeks * 604800;
+		}
+
+		$days = floor($seconds / 86400);
+
+		if (count($str) < $units && ($months > 0 OR $weeks > 0 OR $days > 0))
+		{
+			if ($days > 0)
+			{
+				$str[] = $days.' '.$CI->lang->line($days > 1 ? 'date_days' : 'date_day');
+			}
+
+			$seconds -= $days * 86400;
+		}
+
+		$hours = floor($seconds / 3600);
+
+		if (count($str) < $units && ($days > 0 OR $hours > 0))
+		{
+			if ($hours > 0)
+			{
+				$str[] = $hours.' '.$CI->lang->line($hours > 1 ? 'date_hours' : 'date_hour');
+			}
+
+			$seconds -= $hours * 3600;
+		}
+
+		$minutes = floor($seconds / 60);
+
+		if (count($str) < $units && ($days > 0 OR $hours > 0 OR $minutes > 0))
+		{
+			if ($minutes > 0)
+			{
+				$str[] = $minutes.' '.$CI->lang->line($minutes > 1 ? 'date_minutes' : 'date_minute');
+			}
+
+			$seconds -= $minutes * 60;
+		}
+
+		if (count($str) === 0)
+		{
+			$str[] = $seconds.' '.$CI->lang->line($seconds > 1 ? 'date_seconds' : 'date_second');
+		}
+
+		return implode(', ', $str);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('days_in_month'))
+{
+	/**
+	 * Number of days in a month
+	 *
+	 * Takes a month/year as input and returns the number of days
+	 * for the given month/year. Takes leap years into consideration.
+	 *
+	 * @param	int	a numeric month
+	 * @param	int	a numeric year
+	 * @return	int
+	 */
+	function days_in_month($month = 0, $year = '')
+	{
+		if ($month < 1 OR $month > 12)
+		{
+			return 0;
+		}
+		elseif ( ! is_numeric($year) OR strlen($year) !== 4)
+		{
+			$year = date('Y');
+		}
+
+		if (defined('CAL_GREGORIAN'))
+		{
+			return cal_days_in_month(CAL_GREGORIAN, $month, $year);
+		}
+
+		if ($year >= 1970)
+		{
+			return (int) date('t', mktime(12, 0, 0, $month, 1, $year));
+		}
+
+		if ($month == 2)
+		{
+			if ($year % 400 === 0 OR ($year % 4 === 0 && $year % 100 !== 0))
+			{
+				return 29;
+			}
+		}
+
+		$days_in_month	= array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
+		return $days_in_month[$month - 1];
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('local_to_gmt'))
+{
+	/**
+	 * Converts a local Unix timestamp to GMT
+	 *
+	 * @param	int	Unix timestamp
+	 * @return	int
+	 */
+	function local_to_gmt($time = '')
+	{
+		if ($time === '')
+		{
+			$time = time();
+		}
+
+		return mktime(
+			gmdate('G', $time),
+			gmdate('i', $time),
+			gmdate('s', $time),
+			gmdate('n', $time),
+			gmdate('j', $time),
+			gmdate('Y', $time)
+		);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('gmt_to_local'))
+{
+	/**
+	 * Converts GMT time to a localized value
+	 *
+	 * Takes a Unix timestamp (in GMT) as input, and returns
+	 * at the local value based on the timezone and DST setting
+	 * submitted
+	 *
+	 * @param	int	Unix timestamp
+	 * @param	string	timezone
+	 * @param	bool	whether DST is active
+	 * @return	int
+	 */
+	function gmt_to_local($time = '', $timezone = 'UTC', $dst = FALSE)
+	{
+		if ($time === '')
+		{
+			return now();
+		}
+
+		$time += timezones($timezone) * 3600;
+
+		return ($dst === TRUE) ? $time + 3600 : $time;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('mysql_to_unix'))
+{
+	/**
+	 * Converts a MySQL Timestamp to Unix
+	 *
+	 * @param	int	MySQL timestamp YYYY-MM-DD HH:MM:SS
+	 * @return	int	Unix timstamp
+	 */
+	function mysql_to_unix($time = '')
+	{
+		// We'll remove certain characters for backward compatibility
+		// since the formatting changed with MySQL 4.1
+		// YYYY-MM-DD HH:MM:SS
+
+		$time = str_replace(array('-', ':', ' '), '', $time);
+
+		// YYYYMMDDHHMMSS
+		return mktime(
+			substr($time, 8, 2),
+			substr($time, 10, 2),
+			substr($time, 12, 2),
+			substr($time, 4, 2),
+			substr($time, 6, 2),
+			substr($time, 0, 4)
+		);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('unix_to_human'))
+{
+	/**
+	 * Unix to "Human"
+	 *
+	 * Formats Unix timestamp to the following prototype: 2006-08-21 11:35 PM
+	 *
+	 * @param	int	Unix timestamp
+	 * @param	bool	whether to show seconds
+	 * @param	string	format: us or euro
+	 * @return	string
+	 */
+	function unix_to_human($time = '', $seconds = FALSE, $fmt = 'us')
+	{
+		$r = date('Y', $time).'-'.date('m', $time).'-'.date('d', $time).' ';
+
+		if ($fmt === 'us')
+		{
+			$r .= date('h', $time).':'.date('i', $time);
+		}
+		else
+		{
+			$r .= date('H', $time).':'.date('i', $time);
+		}
+
+		if ($seconds)
+		{
+			$r .= ':'.date('s', $time);
+		}
+
+		if ($fmt === 'us')
+		{
+			return $r.' '.date('A', $time);
+		}
+
+		return $r;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('human_to_unix'))
+{
+	/**
+	 * Convert "human" date to GMT
+	 *
+	 * Reverses the above process
+	 *
+	 * @param	string	format: us or euro
+	 * @return	int
+	 */
+	function human_to_unix($datestr = '')
+	{
+		if ($datestr === '')
+		{
+			return FALSE;
+		}
+
+		$datestr = preg_replace('/\040+/', ' ', trim($datestr));
+
+		if ( ! preg_match('/^(\d{2}|\d{4})\-[0-9]{1,2}\-[0-9]{1,2}\s[0-9]{1,2}:[0-9]{1,2}(?::[0-9]{1,2})?(?:\s[AP]M)?$/i', $datestr))
+		{
+			return FALSE;
+		}
+
+		sscanf($datestr, '%d-%d-%d %s %s', $year, $month, $day, $time, $ampm);
+		sscanf($time, '%d:%d:%d', $hour, $min, $sec);
+		isset($sec) OR $sec = 0;
+
+		if (isset($ampm))
+		{
+			$ampm = strtolower($ampm);
+
+			if ($ampm[0] === 'p' && $hour < 12)
+			{
+				$hour += 12;
+			}
+			elseif ($ampm[0] === 'a' && $hour === 12)
+			{
+				$hour = 0;
+			}
+		}
+
+		return mktime($hour, $min, $sec, $month, $day, $year);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('nice_date'))
+{
+	/**
+	 * Turns many "reasonably-date-like" strings into something
+	 * that is actually useful. This only works for dates after unix epoch.
+	 *
+	 * @deprecated	3.1.3	Use DateTime::createFromFormat($input_format, $input)->format($output_format);
+	 * @param	string	The terribly formatted date-like string
+	 * @param	string	Date format to return (same as php date function)
+	 * @return	string
+	 */
+	function nice_date($bad_date = '', $format = FALSE)
+	{
+		if (empty($bad_date))
+		{
+			return 'Unknown';
+		}
+		elseif (empty($format))
+		{
+			$format = 'U';
+		}
+
+		// Date like: YYYYMM
+		if (preg_match('/^\d{6}$/i', $bad_date))
+		{
+			if (in_array(substr($bad_date, 0, 2), array('19', '20')))
+			{
+				$year  = substr($bad_date, 0, 4);
+				$month = substr($bad_date, 4, 2);
+			}
+			else
+			{
+				$month  = substr($bad_date, 0, 2);
+				$year   = substr($bad_date, 2, 4);
+			}
+
+			return date($format, strtotime($year.'-'.$month.'-01'));
+		}
+
+		// Date Like: YYYYMMDD
+		if (preg_match('/^\d{8}$/i', $bad_date, $matches))
+		{
+			return DateTime::createFromFormat('Ymd', $bad_date)->format($format);
+		}
+
+		// Date Like: MM-DD-YYYY __or__ M-D-YYYY (or anything in between)
+		if (preg_match('/^(\d{1,2})-(\d{1,2})-(\d{4})$/i', $bad_date, $matches))
+		{
+			return date($format, strtotime($matches[3].'-'.$matches[1].'-'.$matches[2]));
+		}
+
+		// Any other kind of string, when converted into UNIX time,
+		// produces "0 seconds after epoc..." is probably bad...
+		// return "Invalid Date".
+		if (date('U', strtotime($bad_date)) === '0')
+		{
+			return 'Invalid Date';
+		}
+
+		// It's probably a valid-ish date format already
+		return date($format, strtotime($bad_date));
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('timezone_menu'))
+{
+	/**
+	 * Timezone Menu
+	 *
+	 * Generates a drop-down menu of timezones.
+	 *
+	 * @param	string	timezone
+	 * @param	string	classname
+	 * @param	string	menu name
+	 * @param	mixed	attributes
+	 * @return	string
+	 */
+	function timezone_menu($default = 'UTC', $class = '', $name = 'timezones', $attributes = '')
+	{
+		$CI =& get_instance();
+		$CI->lang->load('date');
+
+		$default = ($default === 'GMT') ? 'UTC' : $default;
+
+		$menu = '<select name="'.$name.'"';
+
+		if ($class !== '')
+		{
+			$menu .= ' class="'.$class.'"';
+		}
+
+		$menu .= _stringify_attributes($attributes).">\n";
+
+		foreach (timezones() as $key => $val)
+		{
+			$selected = ($default === $key) ? ' selected="selected"' : '';
+			$menu .= '<option value="'.$key.'"'.$selected.'>'.$CI->lang->line($key)."</option>\n";
+		}
+
+		return $menu.'</select>';
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('timezones'))
+{
+	/**
+	 * Timezones
+	 *
+	 * Returns an array of timezones. This is a helper function
+	 * for various other ones in this library
+	 *
+	 * @param	string	timezone
+	 * @return	string
+	 */
+	function timezones($tz = '')
+	{
+		// Note: Don't change the order of these even though
+		// some items appear to be in the wrong order
+
+		$zones = array(
+			'UM12'		=> -12,
+			'UM11'		=> -11,
+			'UM10'		=> -10,
+			'UM95'		=> -9.5,
+			'UM9'		=> -9,
+			'UM8'		=> -8,
+			'UM7'		=> -7,
+			'UM6'		=> -6,
+			'UM5'		=> -5,
+			'UM45'		=> -4.5,
+			'UM4'		=> -4,
+			'UM35'		=> -3.5,
+			'UM3'		=> -3,
+			'UM2'		=> -2,
+			'UM1'		=> -1,
+			'UTC'		=> 0,
+			'UP1'		=> +1,
+			'UP2'		=> +2,
+			'UP3'		=> +3,
+			'UP35'		=> +3.5,
+			'UP4'		=> +4,
+			'UP45'		=> +4.5,
+			'UP5'		=> +5,
+			'UP55'		=> +5.5,
+			'UP575'		=> +5.75,
+			'UP6'		=> +6,
+			'UP65'		=> +6.5,
+			'UP7'		=> +7,
+			'UP8'		=> +8,
+			'UP875'		=> +8.75,
+			'UP9'		=> +9,
+			'UP95'		=> +9.5,
+			'UP10'		=> +10,
+			'UP105'		=> +10.5,
+			'UP11'		=> +11,
+			'UP115'		=> +11.5,
+			'UP12'		=> +12,
+			'UP1275'	=> +12.75,
+			'UP13'		=> +13,
+			'UP14'		=> +14
+		);
+
+		if ($tz === '')
+		{
+			return $zones;
+		}
+
+		return isset($zones[$tz]) ? $zones[$tz] : 0;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('date_range'))
+{
+	/**
+	 * Date range
+	 *
+	 * Returns a list of dates within a specified period.
+	 *
+	 * @param	int	unix_start	UNIX timestamp of period start date
+	 * @param	int	unix_end|days	UNIX timestamp of period end date
+	 *					or interval in days.
+	 * @param	mixed	is_unix		Specifies whether the second parameter
+	 *					is a UNIX timestamp or a day interval
+	 *					 - TRUE or 'unix' for a timestamp
+	 *					 - FALSE or 'days' for an interval
+	 * @param	string  date_format	Output date format, same as in date()
+	 * @return	array
+	 */
+	function date_range($unix_start = '', $mixed = '', $is_unix = TRUE, $format = 'Y-m-d')
+	{
+		if ($unix_start == '' OR $mixed == '' OR $format == '')
+		{
+			return FALSE;
+		}
+
+		$is_unix = ! ( ! $is_unix OR $is_unix === 'days');
+
+		// Validate input and try strtotime() on invalid timestamps/intervals, just in case
+		if ( ( ! ctype_digit((string) $unix_start) && ($unix_start = @strtotime($unix_start)) === FALSE)
+			OR ( ! ctype_digit((string) $mixed) && ($is_unix === FALSE OR ($mixed = @strtotime($mixed)) === FALSE))
+			OR ($is_unix === TRUE && $mixed < $unix_start))
+		{
+			return FALSE;
+		}
+
+		if ($is_unix && ($unix_start == $mixed OR date($format, $unix_start) === date($format, $mixed)))
+		{
+			return array(date($format, $unix_start));
+		}
+
+		$range = array();
+
+		$from = new DateTime();
+		$from->setTimestamp($unix_start);
+
+		if ($is_unix)
+		{
+			$arg = new DateTime();
+			$arg->setTimestamp($mixed);
+		}
+		else
+		{
+			$arg = (int) $mixed;
+		}
+
+		$period = new DatePeriod($from, new DateInterval('P1D'), $arg);
+		foreach ($period as $date)
+		{
+			$range[] = $date->format($format);
+		}
+
+		/* If a period end date was passed to the DatePeriod constructor, it might not
+		 * be in our results. Not sure if this is a bug or it's just possible because
+		 * the end date might actually be less than 24 hours away from the previously
+		 * generated DateTime object, but either way - we have to append it manually.
+		 */
+		if ( ! is_int($arg) && $range[count($range) - 1] !== $arg->format($format))
+		{
+			$range[] = $arg->format($format);
+		}
+
+		return $range;
+	}
+}
diff --git a/system/helpers/directory_helper.php b/system/helpers/directory_helper.php
new file mode 100644
index 0000000..d747a96
--- /dev/null
+++ b/system/helpers/directory_helper.php
@@ -0,0 +1,102 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Directory Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/directory_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('directory_map'))
+{
+	/**
+	 * Create a Directory Map
+	 *
+	 * Reads the specified directory and builds an array
+	 * representation of it. Sub-folders contained with the
+	 * directory will be mapped as well.
+	 *
+	 * @param	string	$source_dir		Path to source
+	 * @param	int	$directory_depth	Depth of directories to traverse
+	 *						(0 = fully recursive, 1 = current dir, etc)
+	 * @param	bool	$hidden			Whether to show hidden files
+	 * @return	array
+	 */
+	function directory_map($source_dir, $directory_depth = 0, $hidden = FALSE)
+	{
+		if ($fp = @opendir($source_dir))
+		{
+			$filedata	= array();
+			$new_depth	= $directory_depth - 1;
+			$source_dir	= rtrim($source_dir, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
+
+			while (FALSE !== ($file = readdir($fp)))
+			{
+				// Remove '.', '..', and hidden files [optional]
+				if ($file === '.' OR $file === '..' OR ($hidden === FALSE && $file[0] === '.'))
+				{
+					continue;
+				}
+
+				is_dir($source_dir.$file) && $file .= DIRECTORY_SEPARATOR;
+
+				if (($directory_depth < 1 OR $new_depth > 0) && is_dir($source_dir.$file))
+				{
+					$filedata[$file] = directory_map($source_dir.$file, $new_depth, $hidden);
+				}
+				else
+				{
+					$filedata[] = $file;
+				}
+			}
+
+			closedir($fp);
+			return $filedata;
+		}
+
+		return FALSE;
+	}
+}
diff --git a/system/helpers/download_helper.php b/system/helpers/download_helper.php
new file mode 100644
index 0000000..9b361c4
--- /dev/null
+++ b/system/helpers/download_helper.php
@@ -0,0 +1,159 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Download Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/download_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('force_download'))
+{
+	/**
+	 * Force Download
+	 *
+	 * Generates headers that force a download to happen
+	 *
+	 * @param	string	filename
+	 * @param	mixed	the data to be downloaded
+	 * @param	bool	whether to try and send the actual file MIME type
+	 * @return	void
+	 */
+	function force_download($filename = '', $data = '', $set_mime = FALSE)
+	{
+		if ($filename === '' OR $data === '')
+		{
+			return;
+		}
+		elseif ($data === NULL)
+		{
+			if ( ! @is_file($filename) OR ($filesize = @filesize($filename)) === FALSE)
+			{
+				return;
+			}
+
+			$filepath = $filename;
+			$filename = explode('/', str_replace(DIRECTORY_SEPARATOR, '/', $filename));
+			$filename = end($filename);
+		}
+		else
+		{
+			$filesize = strlen($data);
+		}
+
+		// Set the default MIME type to send
+		$mime = 'application/octet-stream';
+
+		$x = explode('.', $filename);
+		$extension = end($x);
+
+		if ($set_mime === TRUE)
+		{
+			if (count($x) === 1 OR $extension === '')
+			{
+				/* If we're going to detect the MIME type,
+				 * we'll need a file extension.
+				 */
+				return;
+			}
+
+			// Load the mime types
+			$mimes =& get_mimes();
+
+			// Only change the default MIME if we can find one
+			if (isset($mimes[$extension]))
+			{
+				$mime = is_array($mimes[$extension]) ? $mimes[$extension][0] : $mimes[$extension];
+			}
+		}
+
+		/* It was reported that browsers on Android 2.1 (and possibly older as well)
+		 * need to have the filename extension upper-cased in order to be able to
+		 * download it.
+		 *
+		 * Reference: http://digiblog.de/2011/04/19/android-and-the-download-file-headers/
+		 */
+		if (count($x) !== 1 && isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/Android\s(1|2\.[01])/', $_SERVER['HTTP_USER_AGENT']))
+		{
+			$x[count($x) - 1] = strtoupper($extension);
+			$filename = implode('.', $x);
+		}
+
+		if ($data === NULL && ($fp = @fopen($filepath, 'rb')) === FALSE)
+		{
+			return;
+		}
+
+		// Clean output buffer
+		if (ob_get_level() !== 0 && @ob_end_clean() === FALSE)
+		{
+			@ob_clean();
+		}
+
+		// Generate the server headers
+		header('Content-Type: '.$mime);
+		header('Content-Disposition: attachment; filename="'.$filename.'"');
+		header('Expires: 0');
+		header('Content-Transfer-Encoding: binary');
+		header('Content-Length: '.$filesize);
+		header('Cache-Control: private, no-transform, no-store, must-revalidate');
+
+		// If we have raw data - just dump it
+		if ($data !== NULL)
+		{
+			exit($data);
+		}
+
+		// Flush 1MB chunks of data
+		while ( ! feof($fp) && ($data = fread($fp, 1048576)) !== FALSE)
+		{
+			echo $data;
+		}
+
+		fclose($fp);
+		exit;
+	}
+}
diff --git a/system/helpers/email_helper.php b/system/helpers/email_helper.php
new file mode 100644
index 0000000..ec0c420
--- /dev/null
+++ b/system/helpers/email_helper.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Email Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/email_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('valid_email'))
+{
+	/**
+	 * Validate email address
+	 *
+	 * @deprecated	3.0.0	Use PHP's filter_var() instead
+	 * @param	string	$email
+	 * @return	bool
+	 */
+	function valid_email($email)
+	{
+		return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('send_email'))
+{
+	/**
+	 * Send an email
+	 *
+	 * @deprecated	3.0.0	Use PHP's mail() instead
+	 * @param	string	$recipient
+	 * @param	string	$subject
+	 * @param	string	$message
+	 * @return	bool
+	 */
+	function send_email($recipient, $subject, $message)
+	{
+		return mail($recipient, $subject, $message);
+	}
+}
diff --git a/system/helpers/file_helper.php b/system/helpers/file_helper.php
new file mode 100644
index 0000000..a2adaf2
--- /dev/null
+++ b/system/helpers/file_helper.php
@@ -0,0 +1,454 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter File Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/file_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('read_file'))
+{
+	/**
+	 * Read File
+	 *
+	 * Opens the file specified in the path and returns it as a string.
+	 *
+	 * @todo	Remove in version 3.1+.
+	 * @deprecated	3.0.0	It is now just an alias for PHP's native file_get_contents().
+	 * @param	string	$file	Path to file
+	 * @return	string	File contents
+	 */
+	function read_file($file)
+	{
+		return @file_get_contents($file);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('write_file'))
+{
+	/**
+	 * Write File
+	 *
+	 * Writes data to the file specified in the path.
+	 * Creates a new file if non-existent.
+	 *
+	 * @param	string	$path	File path
+	 * @param	string	$data	Data to write
+	 * @param	string	$mode	fopen() mode (default: 'wb')
+	 * @return	bool
+	 */
+	function write_file($path, $data, $mode = 'wb')
+	{
+		if ( ! $fp = @fopen($path, $mode))
+		{
+			return FALSE;
+		}
+
+		flock($fp, LOCK_EX);
+
+		for ($result = $written = 0, $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);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('delete_files'))
+{
+	/**
+	 * Delete Files
+	 *
+	 * Deletes all files contained in the supplied directory path.
+	 * Files must be writable or owned by the system in order to be deleted.
+	 * If the second parameter is set to TRUE, any directories contained
+	 * within the supplied base directory will be nuked as well.
+	 *
+	 * @param	string	$path		File path
+	 * @param	bool	$del_dir	Whether to delete any directories found in the path
+	 * @param	bool	$htdocs		Whether to skip deleting .htaccess and index page files
+	 * @param	int	$_level		Current directory depth level (default: 0; internal use only)
+	 * @return	bool
+	 */
+	function delete_files($path, $del_dir = FALSE, $htdocs = FALSE, $_level = 0)
+	{
+		// Trim the trailing slash
+		$path = rtrim($path, '/\\');
+
+		if ( ! $current_dir = @opendir($path))
+		{
+			return FALSE;
+		}
+
+		while (FALSE !== ($filename = @readdir($current_dir)))
+		{
+			if ($filename !== '.' && $filename !== '..')
+			{
+				$filepath = $path.DIRECTORY_SEPARATOR.$filename;
+
+				if (is_dir($filepath) && $filename[0] !== '.' && ! is_link($filepath))
+				{
+					delete_files($filepath, $del_dir, $htdocs, $_level + 1);
+				}
+				elseif ($htdocs !== TRUE OR ! preg_match('/^(\.htaccess|index\.(html|htm|php)|web\.config)$/i', $filename))
+				{
+					@unlink($filepath);
+				}
+			}
+		}
+
+		closedir($current_dir);
+
+		return ($del_dir === TRUE && $_level > 0)
+			? @rmdir($path)
+			: TRUE;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('get_filenames'))
+{
+	/**
+	 * Get Filenames
+	 *
+	 * Reads the specified directory and builds an array containing the filenames.
+	 * Any sub-folders contained within the specified path are read as well.
+	 *
+	 * @param	string	path to source
+	 * @param	bool	whether to include the path as part of the filename
+	 * @param	bool	internal variable to determine recursion status - do not use in calls
+	 * @return	array
+	 */
+	function get_filenames($source_dir, $include_path = FALSE, $_recursion = FALSE)
+	{
+		static $_filedata = array();
+
+		if ($fp = @opendir($source_dir))
+		{
+			// reset the array and make sure $source_dir has a trailing slash on the initial call
+			if ($_recursion === FALSE)
+			{
+				$_filedata = array();
+				$source_dir = rtrim(realpath($source_dir), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
+			}
+
+			while (FALSE !== ($file = readdir($fp)))
+			{
+				if (is_dir($source_dir.$file) && $file[0] !== '.')
+				{
+					get_filenames($source_dir.$file.DIRECTORY_SEPARATOR, $include_path, TRUE);
+				}
+				elseif ($file[0] !== '.')
+				{
+					$_filedata[] = ($include_path === TRUE) ? $source_dir.$file : $file;
+				}
+			}
+
+			closedir($fp);
+			return $_filedata;
+		}
+
+		return FALSE;
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('get_dir_file_info'))
+{
+	/**
+	 * Get Directory File Information
+	 *
+	 * Reads the specified directory and builds an array containing the filenames,
+	 * filesize, dates, and permissions
+	 *
+	 * Any sub-folders contained within the specified path are read as well.
+	 *
+	 * @param	string	path to source
+	 * @param	bool	Look only at the top level directory specified?
+	 * @param	bool	internal variable to determine recursion status - do not use in calls
+	 * @return	array
+	 */
+	function get_dir_file_info($source_dir, $top_level_only = TRUE, $_recursion = FALSE)
+	{
+		static $_filedata = array();
+		$relative_path = $source_dir;
+
+		if ($fp = @opendir($source_dir))
+		{
+			// reset the array and make sure $source_dir has a trailing slash on the initial call
+			if ($_recursion === FALSE)
+			{
+				$_filedata = array();
+				$source_dir = rtrim(realpath($source_dir), DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
+			}
+
+			// Used to be foreach (scandir($source_dir, 1) as $file), but scandir() is simply not as fast
+			while (FALSE !== ($file = readdir($fp)))
+			{
+				if (is_dir($source_dir.$file) && $file[0] !== '.' && $top_level_only === FALSE)
+				{
+					get_dir_file_info($source_dir.$file.DIRECTORY_SEPARATOR, $top_level_only, TRUE);
+				}
+				elseif ($file[0] !== '.')
+				{
+					$_filedata[$file] = get_file_info($source_dir.$file);
+					$_filedata[$file]['relative_path'] = $relative_path;
+				}
+			}
+
+			closedir($fp);
+			return $_filedata;
+		}
+
+		return FALSE;
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('get_file_info'))
+{
+	/**
+	 * Get File Info
+	 *
+	 * Given a file and path, returns the name, path, size, date modified
+	 * Second parameter allows you to explicitly declare what information you want returned
+	 * Options are: name, server_path, size, date, readable, writable, executable, fileperms
+	 * Returns FALSE if the file cannot be found.
+	 *
+	 * @param	string	path to file
+	 * @param	mixed	array or comma separated string of information returned
+	 * @return	array
+	 */
+	function get_file_info($file, $returned_values = array('name', 'server_path', 'size', 'date'))
+	{
+		if ( ! file_exists($file))
+		{
+			return FALSE;
+		}
+
+		if (is_string($returned_values))
+		{
+			$returned_values = explode(',', $returned_values);
+		}
+
+		foreach ($returned_values as $key)
+		{
+			switch ($key)
+			{
+				case 'name':
+					$fileinfo['name'] = basename($file);
+					break;
+				case 'server_path':
+					$fileinfo['server_path'] = $file;
+					break;
+				case 'size':
+					$fileinfo['size'] = filesize($file);
+					break;
+				case 'date':
+					$fileinfo['date'] = filemtime($file);
+					break;
+				case 'readable':
+					$fileinfo['readable'] = is_readable($file);
+					break;
+				case 'writable':
+					$fileinfo['writable'] = is_really_writable($file);
+					break;
+				case 'executable':
+					$fileinfo['executable'] = is_executable($file);
+					break;
+				case 'fileperms':
+					$fileinfo['fileperms'] = fileperms($file);
+					break;
+			}
+		}
+
+		return $fileinfo;
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('get_mime_by_extension'))
+{
+	/**
+	 * Get Mime by Extension
+	 *
+	 * Translates a file extension into a mime type based on config/mimes.php.
+	 * Returns FALSE if it can't determine the type, or open the mime config file
+	 *
+	 * Note: this is NOT an accurate way of determining file mime types, and is here strictly as a convenience
+	 * It should NOT be trusted, and should certainly NOT be used for security
+	 *
+	 * @param	string	$filename	File name
+	 * @return	string
+	 */
+	function get_mime_by_extension($filename)
+	{
+		static $mimes;
+
+		if ( ! is_array($mimes))
+		{
+			$mimes = get_mimes();
+
+			if (empty($mimes))
+			{
+				return FALSE;
+			}
+		}
+
+		$extension = strtolower(substr(strrchr($filename, '.'), 1));
+
+		if (isset($mimes[$extension]))
+		{
+			return is_array($mimes[$extension])
+				? current($mimes[$extension]) // Multiple mime types, just give the first one
+				: $mimes[$extension];
+		}
+
+		return FALSE;
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('symbolic_permissions'))
+{
+	/**
+	 * Symbolic Permissions
+	 *
+	 * Takes a numeric value representing a file's permissions and returns
+	 * standard symbolic notation representing that value
+	 *
+	 * @param	int	$perms	Permissions
+	 * @return	string
+	 */
+	function symbolic_permissions($perms)
+	{
+		if (($perms & 0xC000) === 0xC000)
+		{
+			$symbolic = 's'; // Socket
+		}
+		elseif (($perms & 0xA000) === 0xA000)
+		{
+			$symbolic = 'l'; // Symbolic Link
+		}
+		elseif (($perms & 0x8000) === 0x8000)
+		{
+			$symbolic = '-'; // Regular
+		}
+		elseif (($perms & 0x6000) === 0x6000)
+		{
+			$symbolic = 'b'; // Block special
+		}
+		elseif (($perms & 0x4000) === 0x4000)
+		{
+			$symbolic = 'd'; // Directory
+		}
+		elseif (($perms & 0x2000) === 0x2000)
+		{
+			$symbolic = 'c'; // Character special
+		}
+		elseif (($perms & 0x1000) === 0x1000)
+		{
+			$symbolic = 'p'; // FIFO pipe
+		}
+		else
+		{
+			$symbolic = 'u'; // Unknown
+		}
+
+		// Owner
+		$symbolic .= (($perms & 0x0100) ? 'r' : '-')
+			.(($perms & 0x0080) ? 'w' : '-')
+			.(($perms & 0x0040) ? (($perms & 0x0800) ? 's' : 'x' ) : (($perms & 0x0800) ? 'S' : '-'));
+
+		// Group
+		$symbolic .= (($perms & 0x0020) ? 'r' : '-')
+			.(($perms & 0x0010) ? 'w' : '-')
+			.(($perms & 0x0008) ? (($perms & 0x0400) ? 's' : 'x' ) : (($perms & 0x0400) ? 'S' : '-'));
+
+		// World
+		$symbolic .= (($perms & 0x0004) ? 'r' : '-')
+			.(($perms & 0x0002) ? 'w' : '-')
+			.(($perms & 0x0001) ? (($perms & 0x0200) ? 't' : 'x' ) : (($perms & 0x0200) ? 'T' : '-'));
+
+		return $symbolic;
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('octal_permissions'))
+{
+	/**
+	 * Octal Permissions
+	 *
+	 * Takes a numeric value representing a file's permissions and returns
+	 * a three character string representing the file's octal permissions
+	 *
+	 * @param	int	$perms	Permissions
+	 * @return	string
+	 */
+	function octal_permissions($perms)
+	{
+		return substr(sprintf('%o', $perms), -3);
+	}
+}
diff --git a/system/helpers/form_helper.php b/system/helpers/form_helper.php
new file mode 100644
index 0000000..ba74ff5
--- /dev/null
+++ b/system/helpers/form_helper.php
@@ -0,0 +1,1056 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Form Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/form_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_open'))
+{
+	/**
+	 * Form Declaration
+	 *
+	 * Creates the opening portion of the form.
+	 *
+	 * @param	string	the URI segments of the form destination
+	 * @param	array	a key/value pair of attributes
+	 * @param	array	a key/value pair hidden data
+	 * @return	string
+	 */
+	function form_open($action = '', $attributes = array(), $hidden = array())
+	{
+		$CI =& get_instance();
+
+		// If no action is provided then set to the current url
+		if ( ! $action)
+		{
+			$action = $CI->config->site_url($CI->uri->uri_string());
+		}
+		// If an action is not a full URL then turn it into one
+		elseif (strpos($action, '://') === FALSE)
+		{
+			$action = $CI->config->site_url($action);
+		}
+
+		$attributes = _attributes_to_string($attributes);
+
+		if (stripos($attributes, 'method=') === FALSE)
+		{
+			$attributes .= ' method="post"';
+		}
+
+		if (stripos($attributes, 'accept-charset=') === FALSE)
+		{
+			$attributes .= ' accept-charset="'.strtolower(config_item('charset')).'"';
+		}
+
+		$form = '<form action="'.$action.'"'.$attributes.">\n";
+
+		if (is_array($hidden))
+		{
+			foreach ($hidden as $name => $value)
+			{
+				$form .= '<input type="hidden" name="'.$name.'" value="'.html_escape($value).'" />'."\n";
+			}
+		}
+
+		// Add CSRF field if enabled, but leave it out for GET requests and requests to external websites
+		if ($CI->config->item('csrf_protection') === TRUE && strpos($action, $CI->config->base_url()) !== FALSE && ! stripos($form, 'method="get"'))
+		{
+			// Prepend/append random-length "white noise" around the CSRF
+			// token input, as a form of protection against BREACH attacks
+			if (FALSE !== ($noise = $CI->security->get_random_bytes(1)))
+			{
+				list(, $noise) = unpack('c', $noise);
+			}
+			else
+			{
+				$noise = mt_rand(-128, 127);
+			}
+
+			// Prepend if $noise has a negative value, append if positive, do nothing for zero
+			$prepend = $append = '';
+			if ($noise < 0)
+			{
+				$prepend = str_repeat(" ", abs($noise));
+			}
+			elseif ($noise > 0)
+			{
+				$append  = str_repeat(" ", $noise);
+			}
+
+			$form .= sprintf(
+				'%s<input type="hidden" name="%s" value="%s" />%s%s',
+				$prepend,
+				$CI->security->get_csrf_token_name(),
+				$CI->security->get_csrf_hash(),
+				$append,
+				"\n"
+			);
+		}
+
+		return $form;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_open_multipart'))
+{
+	/**
+	 * Form Declaration - Multipart type
+	 *
+	 * Creates the opening portion of the form, but with "multipart/form-data".
+	 *
+	 * @param	string	the URI segments of the form destination
+	 * @param	array	a key/value pair of attributes
+	 * @param	array	a key/value pair hidden data
+	 * @return	string
+	 */
+	function form_open_multipart($action = '', $attributes = array(), $hidden = array())
+	{
+		if (is_string($attributes))
+		{
+			$attributes .= ' enctype="multipart/form-data"';
+		}
+		else
+		{
+			$attributes['enctype'] = 'multipart/form-data';
+		}
+
+		return form_open($action, $attributes, $hidden);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_hidden'))
+{
+	/**
+	 * Hidden Input Field
+	 *
+	 * Generates hidden fields. You can pass a simple key/value string or
+	 * an associative array with multiple values.
+	 *
+	 * @param	mixed	$name		Field name
+	 * @param	string	$value		Field value
+	 * @param	bool	$recursing
+	 * @return	string
+	 */
+	function form_hidden($name, $value = '', $recursing = FALSE)
+	{
+		static $form;
+
+		if ($recursing === FALSE)
+		{
+			$form = "\n";
+		}
+
+		if (is_array($name))
+		{
+			foreach ($name as $key => $val)
+			{
+				form_hidden($key, $val, TRUE);
+			}
+
+			return $form;
+		}
+
+		if ( ! is_array($value))
+		{
+			$form .= '<input type="hidden" name="'.$name.'" value="'.html_escape($value)."\" />\n";
+		}
+		else
+		{
+			foreach ($value as $k => $v)
+			{
+				$k = is_int($k) ? '' : $k;
+				form_hidden($name.'['.$k.']', $v, TRUE);
+			}
+		}
+
+		return $form;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_input'))
+{
+	/**
+	 * Text Input Field
+	 *
+	 * @param	mixed
+	 * @param	string
+	 * @param	mixed
+	 * @return	string
+	 */
+	function form_input($data = '', $value = '', $extra = '')
+	{
+		$defaults = array(
+			'type' => 'text',
+			'name' => is_array($data) ? '' : $data,
+			'value' => $value
+		);
+
+		return '<input '._parse_form_attributes($data, $defaults)._attributes_to_string($extra)." />\n";
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_password'))
+{
+	/**
+	 * Password Field
+	 *
+	 * Identical to the input function but adds the "password" type
+	 *
+	 * @param	mixed
+	 * @param	string
+	 * @param	mixed
+	 * @return	string
+	 */
+	function form_password($data = '', $value = '', $extra = '')
+	{
+		is_array($data) OR $data = array('name' => $data);
+		$data['type'] = 'password';
+		return form_input($data, $value, $extra);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_upload'))
+{
+	/**
+	 * Upload Field
+	 *
+	 * Identical to the input function but adds the "file" type
+	 *
+	 * @param	mixed
+	 * @param	string
+	 * @param	mixed
+	 * @return	string
+	 */
+	function form_upload($data = '', $value = '', $extra = '')
+	{
+		$defaults = array('type' => 'file', 'name' => '');
+		is_array($data) OR $data = array('name' => $data);
+		$data['type'] = 'file';
+
+		return '<input '._parse_form_attributes($data, $defaults)._attributes_to_string($extra)." />\n";
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_textarea'))
+{
+	/**
+	 * Textarea field
+	 *
+	 * @param	mixed	$data
+	 * @param	string	$value
+	 * @param	mixed	$extra
+	 * @return	string
+	 */
+	function form_textarea($data = '', $value = '', $extra = '')
+	{
+		$defaults = array(
+			'name' => is_array($data) ? '' : $data,
+			'cols' => '40',
+			'rows' => '10'
+		);
+
+		if ( ! is_array($data) OR ! isset($data['value']))
+		{
+			$val = $value;
+		}
+		else
+		{
+			$val = $data['value'];
+			unset($data['value']); // textareas don't use the value attribute
+		}
+
+		return '<textarea '._parse_form_attributes($data, $defaults)._attributes_to_string($extra).'>'
+			.html_escape($val)
+			."</textarea>\n";
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_multiselect'))
+{
+	/**
+	 * Multi-select menu
+	 *
+	 * @param	string
+	 * @param	array
+	 * @param	mixed
+	 * @param	mixed
+	 * @return	string
+	 */
+	function form_multiselect($name = '', $options = array(), $selected = array(), $extra = '')
+	{
+		$extra = _attributes_to_string($extra);
+		if (stripos($extra, 'multiple') === FALSE)
+		{
+			$extra .= ' multiple="multiple"';
+		}
+
+		return form_dropdown($name, $options, $selected, $extra);
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('form_dropdown'))
+{
+	/**
+	 * Drop-down Menu
+	 *
+	 * @param	mixed	$data
+	 * @param	mixed	$options
+	 * @param	mixed	$selected
+	 * @param	mixed	$extra
+	 * @return	string
+	 */
+	function form_dropdown($data = '', $options = array(), $selected = array(), $extra = '')
+	{
+		$defaults = array();
+
+		if (is_array($data))
+		{
+			if (isset($data['selected']))
+			{
+				$selected = $data['selected'];
+				unset($data['selected']); // select tags don't have a selected attribute
+			}
+
+			if (isset($data['options']))
+			{
+				$options = $data['options'];
+				unset($data['options']); // select tags don't use an options attribute
+			}
+		}
+		else
+		{
+			$defaults = array('name' => $data);
+		}
+
+		is_array($selected) OR $selected = array($selected);
+		is_array($options) OR $options = array($options);
+
+		// If no selected state was submitted we will attempt to set it automatically
+		if (empty($selected))
+		{
+			if (is_array($data))
+			{
+				if (isset($data['name'], $_POST[$data['name']]))
+				{
+					$selected = array($_POST[$data['name']]);
+				}
+			}
+			elseif (isset($_POST[$data]))
+			{
+				$selected = array($_POST[$data]);
+			}
+		}
+
+		$extra = _attributes_to_string($extra);
+
+		$multiple = (count($selected) > 1 && stripos($extra, 'multiple') === FALSE) ? ' multiple="multiple"' : '';
+
+		$form = '<select '.rtrim(_parse_form_attributes($data, $defaults)).$extra.$multiple.">\n";
+
+		foreach ($options as $key => $val)
+		{
+			$key = (string) $key;
+
+			if (is_array($val))
+			{
+				if (empty($val))
+				{
+					continue;
+				}
+
+				$form .= '<optgroup label="'.$key."\">\n";
+
+				foreach ($val as $optgroup_key => $optgroup_val)
+				{
+					$sel = in_array($optgroup_key, $selected) ? ' selected="selected"' : '';
+					$form .= '<option value="'.html_escape($optgroup_key).'"'.$sel.'>'
+						.(string) $optgroup_val."</option>\n";
+				}
+
+				$form .= "</optgroup>\n";
+			}
+			else
+			{
+				$form .= '<option value="'.html_escape($key).'"'
+					.(in_array($key, $selected) ? ' selected="selected"' : '').'>'
+					.(string) $val."</option>\n";
+			}
+		}
+
+		return $form."</select>\n";
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_checkbox'))
+{
+	/**
+	 * Checkbox Field
+	 *
+	 * @param	mixed
+	 * @param	string
+	 * @param	bool
+	 * @param	mixed
+	 * @return	string
+	 */
+	function form_checkbox($data = '', $value = '', $checked = FALSE, $extra = '')
+	{
+		$defaults = array('type' => 'checkbox', 'name' => ( ! is_array($data) ? $data : ''), 'value' => $value);
+
+		if (is_array($data) && array_key_exists('checked', $data))
+		{
+			$checked = $data['checked'];
+
+			if ($checked == FALSE)
+			{
+				unset($data['checked']);
+			}
+			else
+			{
+				$data['checked'] = 'checked';
+			}
+		}
+
+		if ($checked == TRUE)
+		{
+			$defaults['checked'] = 'checked';
+		}
+		else
+		{
+			unset($defaults['checked']);
+		}
+
+		return '<input '._parse_form_attributes($data, $defaults)._attributes_to_string($extra)." />\n";
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_radio'))
+{
+	/**
+	 * Radio Button
+	 *
+	 * @param	mixed
+	 * @param	string
+	 * @param	bool
+	 * @param	mixed
+	 * @return	string
+	 */
+	function form_radio($data = '', $value = '', $checked = FALSE, $extra = '')
+	{
+		is_array($data) OR $data = array('name' => $data);
+		$data['type'] = 'radio';
+
+		return form_checkbox($data, $value, $checked, $extra);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_submit'))
+{
+	/**
+	 * Submit Button
+	 *
+	 * @param	mixed
+	 * @param	string
+	 * @param	mixed
+	 * @return	string
+	 */
+	function form_submit($data = '', $value = '', $extra = '')
+	{
+		$defaults = array(
+			'type' => 'submit',
+			'name' => is_array($data) ? '' : $data,
+			'value' => $value
+		);
+
+		return '<input '._parse_form_attributes($data, $defaults)._attributes_to_string($extra)." />\n";
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_reset'))
+{
+	/**
+	 * Reset Button
+	 *
+	 * @param	mixed
+	 * @param	string
+	 * @param	mixed
+	 * @return	string
+	 */
+	function form_reset($data = '', $value = '', $extra = '')
+	{
+		$defaults = array(
+			'type' => 'reset',
+			'name' => is_array($data) ? '' : $data,
+			'value' => $value
+		);
+
+		return '<input '._parse_form_attributes($data, $defaults)._attributes_to_string($extra)." />\n";
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_button'))
+{
+	/**
+	 * Form Button
+	 *
+	 * @param	mixed
+	 * @param	string
+	 * @param	mixed
+	 * @return	string
+	 */
+	function form_button($data = '', $content = '', $extra = '')
+	{
+		$defaults = array(
+			'name' => is_array($data) ? '' : $data,
+			'type' => 'button'
+		);
+
+		if (is_array($data) && isset($data['content']))
+		{
+			$content = $data['content'];
+			unset($data['content']); // content is not an attribute
+		}
+
+		return '<button '._parse_form_attributes($data, $defaults)._attributes_to_string($extra).'>'
+			.$content
+			."</button>\n";
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_label'))
+{
+	/**
+	 * Form Label Tag
+	 *
+	 * @param	string	The text to appear onscreen
+	 * @param	string	The id the label applies to
+	 * @param	mixed	Additional attributes
+	 * @return	string
+	 */
+	function form_label($label_text = '', $id = '', $attributes = array())
+	{
+
+		$label = '<label';
+
+		if ($id !== '')
+		{
+			$label .= ' for="'.$id.'"';
+		}
+
+		$label .= _attributes_to_string($attributes);
+
+		return $label.'>'.$label_text.'</label>';
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_fieldset'))
+{
+	/**
+	 * Fieldset Tag
+	 *
+	 * Used to produce <fieldset><legend>text</legend>.  To close fieldset
+	 * use form_fieldset_close()
+	 *
+	 * @param	string	The legend text
+	 * @param	array	Additional attributes
+	 * @return	string
+	 */
+	function form_fieldset($legend_text = '', $attributes = array())
+	{
+		$fieldset = '<fieldset'._attributes_to_string($attributes).">\n";
+		if ($legend_text !== '')
+		{
+			return $fieldset.'<legend>'.$legend_text."</legend>\n";
+		}
+
+		return $fieldset;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_fieldset_close'))
+{
+	/**
+	 * Fieldset Close Tag
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	function form_fieldset_close($extra = '')
+	{
+		return '</fieldset>'.$extra;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_close'))
+{
+	/**
+	 * Form Close Tag
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	function form_close($extra = '')
+	{
+		return '</form>'.$extra;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_prep'))
+{
+	/**
+	 * Form Prep
+	 *
+	 * Formats text so that it can be safely placed in a form field in the event it has HTML tags.
+	 *
+	 * @deprecated	3.0.0	An alias for html_escape()
+	 * @param	string|string[]	$str		Value to escape
+	 * @return	string|string[]	Escaped values
+	 */
+	function form_prep($str)
+	{
+		return html_escape($str, TRUE);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('set_value'))
+{
+	/**
+	 * Form Value
+	 *
+	 * Grabs a value from the POST array for the specified field so you can
+	 * re-populate an input field or textarea. If Form Validation
+	 * is active it retrieves the info from the validation class
+	 *
+	 * @param	string	$field		Field name
+	 * @param	string	$default	Default value
+	 * @param	bool	$html_escape	Whether to escape HTML special characters or not
+	 * @return	string
+	 */
+	function set_value($field, $default = '', $html_escape = TRUE)
+	{
+		$CI =& get_instance();
+
+		$value = (isset($CI->form_validation) && is_object($CI->form_validation) && $CI->form_validation->has_rule($field))
+			? $CI->form_validation->set_value($field, $default)
+			: $CI->input->post($field, FALSE);
+
+		isset($value) OR $value = $default;
+		return ($html_escape) ? html_escape($value) : $value;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('set_select'))
+{
+	/**
+	 * Set Select
+	 *
+	 * Let's you set the selected value of a <select> menu via data in the POST array.
+	 * If Form Validation is active it retrieves the info from the validation class
+	 *
+	 * @param	string
+	 * @param	string
+	 * @param	bool
+	 * @return	string
+	 */
+	function set_select($field, $value = '', $default = FALSE)
+	{
+		$CI =& get_instance();
+
+		if (isset($CI->form_validation) && is_object($CI->form_validation) && $CI->form_validation->has_rule($field))
+		{
+			return $CI->form_validation->set_select($field, $value, $default);
+		}
+		elseif (($input = $CI->input->post($field, FALSE)) === NULL)
+		{
+			return ($default === TRUE) ? ' selected="selected"' : '';
+		}
+
+		$value = (string) $value;
+		if (is_array($input))
+		{
+			// Note: in_array('', array(0)) returns TRUE, do not use it
+			foreach ($input as &$v)
+			{
+				if ($value === $v)
+				{
+					return ' selected="selected"';
+				}
+			}
+
+			return '';
+		}
+
+		return ($input === $value) ? ' selected="selected"' : '';
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('set_checkbox'))
+{
+	/**
+	 * Set Checkbox
+	 *
+	 * Let's you set the selected value of a checkbox via the value in the POST array.
+	 * If Form Validation is active it retrieves the info from the validation class
+	 *
+	 * @param	string
+	 * @param	string
+	 * @param	bool
+	 * @return	string
+	 */
+	function set_checkbox($field, $value = '', $default = FALSE)
+	{
+		$CI =& get_instance();
+
+		if (isset($CI->form_validation) && is_object($CI->form_validation) && $CI->form_validation->has_rule($field))
+		{
+			return $CI->form_validation->set_checkbox($field, $value, $default);
+		}
+
+		// Form inputs are always strings ...
+		$value = (string) $value;
+		$input = $CI->input->post($field, FALSE);
+
+		if (is_array($input))
+		{
+			// Note: in_array('', array(0)) returns TRUE, do not use it
+			foreach ($input as &$v)
+			{
+				if ($value === $v)
+				{
+					return ' checked="checked"';
+				}
+			}
+
+			return '';
+		}
+
+		// Unchecked checkbox and radio inputs are not even submitted by browsers ...
+		if ($CI->input->method() === 'post')
+		{
+			return ($input === $value) ? ' checked="checked"' : '';
+		}
+
+		return ($default === TRUE) ? ' checked="checked"' : '';
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('set_radio'))
+{
+	/**
+	 * Set Radio
+	 *
+	 * Let's you set the selected value of a radio field via info in the POST array.
+	 * If Form Validation is active it retrieves the info from the validation class
+	 *
+	 * @param	string	$field
+	 * @param	string	$value
+	 * @param	bool	$default
+	 * @return	string
+	 */
+	function set_radio($field, $value = '', $default = FALSE)
+	{
+		$CI =& get_instance();
+
+		if (isset($CI->form_validation) && is_object($CI->form_validation) && $CI->form_validation->has_rule($field))
+		{
+			return $CI->form_validation->set_radio($field, $value, $default);
+		}
+
+		// Form inputs are always strings ...
+		$value = (string) $value;
+		$input = $CI->input->post($field, FALSE);
+
+		if (is_array($input))
+		{
+			// Note: in_array('', array(0)) returns TRUE, do not use it
+			foreach ($input as &$v)
+			{
+				if ($value === $v)
+				{
+					return ' checked="checked"';
+				}
+			}
+
+			return '';
+		}
+
+		// Unchecked checkbox and radio inputs are not even submitted by browsers ...
+		if ($CI->input->method() === 'post')
+		{
+			return ($input === $value) ? ' checked="checked"' : '';
+		}
+
+		return ($default === TRUE) ? ' checked="checked"' : '';
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('form_error'))
+{
+	/**
+	 * Form Error
+	 *
+	 * Returns the error for a specific form field. This is a helper for the
+	 * form validation class.
+	 *
+	 * @param	string
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	function form_error($field = '', $prefix = '', $suffix = '')
+	{
+		if (FALSE === ($OBJ =& _get_validation_object()))
+		{
+			return '';
+		}
+
+		return $OBJ->error($field, $prefix, $suffix);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('validation_errors'))
+{
+	/**
+	 * Validation Error String
+	 *
+	 * Returns all the errors associated with a form submission. This is a helper
+	 * function for the form validation class.
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	function validation_errors($prefix = '', $suffix = '')
+	{
+		if (FALSE === ($OBJ =& _get_validation_object()))
+		{
+			return '';
+		}
+
+		return $OBJ->error_string($prefix, $suffix);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('_parse_form_attributes'))
+{
+	/**
+	 * Parse the form attributes
+	 *
+	 * Helper function used by some of the form helpers
+	 *
+	 * @param	array	$attributes	List of attributes
+	 * @param	array	$default	Default values
+	 * @return	string
+	 */
+	function _parse_form_attributes($attributes, $default)
+	{
+		if (is_array($attributes))
+		{
+			foreach ($default as $key => $val)
+			{
+				if (isset($attributes[$key]))
+				{
+					$default[$key] = $attributes[$key];
+					unset($attributes[$key]);
+				}
+			}
+
+			if (count($attributes) > 0)
+			{
+				$default = array_merge($default, $attributes);
+			}
+		}
+
+		$att = '';
+
+		foreach ($default as $key => $val)
+		{
+			if ($key === 'value')
+			{
+				$val = html_escape($val);
+			}
+			elseif ($key === 'name' && ! strlen($default['name']))
+			{
+				continue;
+			}
+
+			$att .= $key.'="'.$val.'" ';
+		}
+
+		return $att;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('_attributes_to_string'))
+{
+	/**
+	 * Attributes To String
+	 *
+	 * Helper function used by some of the form helpers
+	 *
+	 * @param	mixed
+	 * @return	string
+	 */
+	function _attributes_to_string($attributes)
+	{
+		if (empty($attributes))
+		{
+			return '';
+		}
+
+		if (is_object($attributes))
+		{
+			$attributes = (array) $attributes;
+		}
+
+		if (is_array($attributes))
+		{
+			$atts = '';
+
+			foreach ($attributes as $key => $val)
+			{
+				$atts .= ' '.$key.'="'.$val.'"';
+			}
+
+			return $atts;
+		}
+
+		if (is_string($attributes))
+		{
+			return ' '.$attributes;
+		}
+
+		return FALSE;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('_get_validation_object'))
+{
+	/**
+	 * Validation Object
+	 *
+	 * Determines what the form validation class was instantiated as, fetches
+	 * the object and returns it.
+	 *
+	 * @return	mixed
+	 */
+	function &_get_validation_object()
+	{
+		$CI =& get_instance();
+
+		// We set this as a variable since we're returning by reference.
+		$return = FALSE;
+
+		if (FALSE !== ($object = $CI->load->is_loaded('Form_validation')))
+		{
+			if ( ! isset($CI->$object) OR ! is_object($CI->$object))
+			{
+				return $return;
+			}
+
+			return $CI->$object;
+		}
+
+		return $return;
+	}
+}
diff --git a/system/helpers/html_helper.php b/system/helpers/html_helper.php
new file mode 100644
index 0000000..93ecb1d
--- /dev/null
+++ b/system/helpers/html_helper.php
@@ -0,0 +1,410 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter HTML Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/html_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('heading'))
+{
+	/**
+	 * Heading
+	 *
+	 * Generates an HTML heading tag.
+	 *
+	 * @param	string	content
+	 * @param	int	heading level
+	 * @param	string
+	 * @return	string
+	 */
+	function heading($data = '', $h = '1', $attributes = '')
+	{
+		return '<h'.$h._stringify_attributes($attributes).'>'.$data.'</h'.$h.'>';
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('ul'))
+{
+	/**
+	 * Unordered List
+	 *
+	 * Generates an HTML unordered list from an single or multi-dimensional array.
+	 *
+	 * @param	array
+	 * @param	mixed
+	 * @return	string
+	 */
+	function ul($list, $attributes = '')
+	{
+		return _list('ul', $list, $attributes);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('ol'))
+{
+	/**
+	 * Ordered List
+	 *
+	 * Generates an HTML ordered list from an single or multi-dimensional array.
+	 *
+	 * @param	array
+	 * @param	mixed
+	 * @return	string
+	 */
+	function ol($list, $attributes = '')
+	{
+		return _list('ol', $list, $attributes);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('_list'))
+{
+	/**
+	 * Generates the list
+	 *
+	 * Generates an HTML ordered list from an single or multi-dimensional array.
+	 *
+	 * @param	string
+	 * @param	mixed
+	 * @param	mixed
+	 * @param	int
+	 * @return	string
+	 */
+	function _list($type = 'ul', $list = array(), $attributes = '', $depth = 0)
+	{
+		// If an array wasn't submitted there's nothing to do...
+		if ( ! is_array($list))
+		{
+			return $list;
+		}
+
+		// Set the indentation based on the depth
+		$out = str_repeat(' ', $depth)
+			// Write the opening list tag
+			.'<'.$type._stringify_attributes($attributes).">\n";
+
+		// Cycle through the list elements.  If an array is
+		// encountered we will recursively call _list()
+
+		static $_last_list_item = '';
+		foreach ($list as $key => $val)
+		{
+			$_last_list_item = $key;
+
+			$out .= str_repeat(' ', $depth + 2).'<li>';
+
+			if ( ! is_array($val))
+			{
+				$out .= $val;
+			}
+			else
+			{
+				$out .= $_last_list_item."\n"._list($type, $val, '', $depth + 4).str_repeat(' ', $depth + 2);
+			}
+
+			$out .= "</li>\n";
+		}
+
+		// Set the indentation for the closing tag and apply it
+		return $out.str_repeat(' ', $depth).'</'.$type.">\n";
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('img'))
+{
+	/**
+	 * Image
+	 *
+	 * Generates an <img /> element
+	 *
+	 * @param	mixed
+	 * @param	bool
+	 * @param	mixed
+	 * @return	string
+	 */
+	function img($src = '', $index_page = FALSE, $attributes = '')
+	{
+		if ( ! is_array($src) )
+		{
+			$src = array('src' => $src);
+		}
+
+		// If there is no alt attribute defined, set it to an empty string
+		if ( ! isset($src['alt']))
+		{
+			$src['alt'] = '';
+		}
+
+		$img = '<img';
+
+		foreach ($src as $k => $v)
+		{
+			if ($k === 'src' && ! preg_match('#^(data:[a-z,;])|(([a-z]+:)?(?<!data:)//)#i', $v))
+			{
+				if ($index_page === TRUE)
+				{
+					$img .= ' src="'.get_instance()->config->site_url($v).'"';
+				}
+				else
+				{
+					$img .= ' src="'.get_instance()->config->base_url($v).'"';
+				}
+			}
+			else
+			{
+				$img .= ' '.$k.'="'.$v.'"';
+			}
+		}
+
+		return $img._stringify_attributes($attributes).' />';
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('doctype'))
+{
+	/**
+	 * Doctype
+	 *
+	 * Generates a page document type declaration
+	 *
+	 * Examples of valid options: html5, xhtml-11, xhtml-strict, xhtml-trans,
+	 * xhtml-frame, html4-strict, html4-trans, and html4-frame.
+	 * All values are saved in the doctypes config file.
+	 *
+	 * @param	string	type	The doctype to be generated
+	 * @return	string
+	 */
+	function doctype($type = 'xhtml1-strict')
+	{
+		static $doctypes;
+
+		if ( ! is_array($doctypes))
+		{
+			if (file_exists(APPPATH.'config/doctypes.php'))
+			{
+				include(APPPATH.'config/doctypes.php');
+			}
+
+			if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/doctypes.php'))
+			{
+				include(APPPATH.'config/'.ENVIRONMENT.'/doctypes.php');
+			}
+
+			if (empty($_doctypes) OR ! is_array($_doctypes))
+			{
+				$doctypes = array();
+				return FALSE;
+			}
+
+			$doctypes = $_doctypes;
+		}
+
+		return isset($doctypes[$type]) ? $doctypes[$type] : FALSE;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('link_tag'))
+{
+	/**
+	 * Link
+	 *
+	 * Generates link to a CSS file
+	 *
+	 * @param	mixed	stylesheet hrefs or an array
+	 * @param	string	rel
+	 * @param	string	type
+	 * @param	string	title
+	 * @param	string	media
+	 * @param	bool	should index_page be added to the css path
+	 * @return	string
+	 */
+	function link_tag($href = '', $rel = 'stylesheet', $type = 'text/css', $title = '', $media = '', $index_page = FALSE)
+	{
+		$CI =& get_instance();
+		$link = '<link ';
+
+		if (is_array($href))
+		{
+			foreach ($href as $k => $v)
+			{
+				if ($k === 'href' && ! preg_match('#^([a-z]+:)?//#i', $v))
+				{
+					if ($index_page === TRUE)
+					{
+						$link .= 'href="'.$CI->config->site_url($v).'" ';
+					}
+					else
+					{
+						$link .= 'href="'.$CI->config->base_url($v).'" ';
+					}
+				}
+				else
+				{
+					$link .= $k.'="'.$v.'" ';
+				}
+			}
+		}
+		else
+		{
+			if (preg_match('#^([a-z]+:)?//#i', $href))
+			{
+				$link .= 'href="'.$href.'" ';
+			}
+			elseif ($index_page === TRUE)
+			{
+				$link .= 'href="'.$CI->config->site_url($href).'" ';
+			}
+			else
+			{
+				$link .= 'href="'.$CI->config->base_url($href).'" ';
+			}
+
+			$link .= 'rel="'.$rel.'" type="'.$type.'" ';
+
+			if ($media !== '')
+			{
+				$link .= 'media="'.$media.'" ';
+			}
+
+			if ($title !== '')
+			{
+				$link .= 'title="'.$title.'" ';
+			}
+		}
+
+		return $link."/>\n";
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('meta'))
+{
+	/**
+	 * Generates meta tags from an array of key/values
+	 *
+	 * @param	array
+	 * @param	string
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	function meta($name = '', $content = '', $type = 'name', $newline = "\n")
+	{
+		// Since we allow the data to be passes as a string, a simple array
+		// or a multidimensional one, we need to do a little prepping.
+		if ( ! is_array($name))
+		{
+			$name = array(array('name' => $name, 'content' => $content, 'type' => $type, 'newline' => $newline));
+		}
+		elseif (isset($name['name']))
+		{
+			// Turn single array into multidimensional
+			$name = array($name);
+		}
+
+		$str = '';
+		foreach ($name as $meta)
+		{
+			$type		= (isset($meta['type']) && $meta['type'] !== 'name')	? 'http-equiv' : 'name';
+			$name		= isset($meta['name'])					? $meta['name'] : '';
+			$content	= isset($meta['content'])				? $meta['content'] : '';
+			$newline	= isset($meta['newline'])				? $meta['newline'] : "\n";
+
+			$str .= '<meta '.$type.'="'.$name.'" content="'.$content.'" />'.$newline;
+		}
+
+		return $str;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('br'))
+{
+	/**
+	 * Generates HTML BR tags based on number supplied
+	 *
+	 * @deprecated	3.0.0	Use str_repeat() instead
+	 * @param	int	$count	Number of times to repeat the tag
+	 * @return	string
+	 */
+	function br($count = 1)
+	{
+		return str_repeat('<br />', $count);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('nbs'))
+{
+	/**
+	 * Generates non-breaking space entities based on number supplied
+	 *
+	 * @deprecated	3.0.0	Use str_repeat() instead
+	 * @param	int
+	 * @return	string
+	 */
+	function nbs($num = 1)
+	{
+		return str_repeat('&nbsp;', $num);
+	}
+}
diff --git a/system/helpers/index.html b/system/helpers/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/helpers/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/system/helpers/inflector_helper.php b/system/helpers/inflector_helper.php
new file mode 100644
index 0000000..91a5d84
--- /dev/null
+++ b/system/helpers/inflector_helper.php
@@ -0,0 +1,288 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Inflector Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/inflector_helper.html
+ */
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('singular'))
+{
+	/**
+	 * Singular
+	 *
+	 * Takes a plural word and makes it singular
+	 *
+	 * @param	string	$str	Input string
+	 * @return	string
+	 */
+	function singular($str)
+	{
+		$result = strval($str);
+
+		if ( ! word_is_countable($result))
+		{
+			return $result;
+		}
+
+		$singular_rules = array(
+			'/(matr)ices$/'		=> '\1ix',
+			'/(vert|ind)ices$/'	=> '\1ex',
+			'/^(ox)en/'		=> '\1',
+			'/(alias)es$/'		=> '\1',
+			'/([octop|vir])i$/'	=> '\1us',
+			'/(cris|ax|test)es$/'	=> '\1is',
+			'/(shoe)s$/'		=> '\1',
+			'/(o)es$/'		=> '\1',
+			'/(bus|campus)es$/'	=> '\1',
+			'/([m|l])ice$/'		=> '\1ouse',
+			'/(x|ch|ss|sh)es$/'	=> '\1',
+			'/(m)ovies$/'		=> '\1\2ovie',
+			'/(s)eries$/'		=> '\1\2eries',
+			'/([^aeiouy]|qu)ies$/'	=> '\1y',
+			'/([lr])ves$/'		=> '\1f',
+			'/(tive)s$/'		=> '\1',
+			'/(hive)s$/'		=> '\1',
+			'/([^f])ves$/'		=> '\1fe',
+			'/(^analy)ses$/'	=> '\1sis',
+			'/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/' => '\1\2sis',
+			'/([ti])a$/'		=> '\1um',
+			'/(p)eople$/'		=> '\1\2erson',
+			'/(m)en$/'		=> '\1an',
+			'/(s)tatuses$/'		=> '\1\2tatus',
+			'/(c)hildren$/'		=> '\1\2hild',
+			'/(n)ews$/'		=> '\1\2ews',
+			'/(quiz)zes$/'		=> '\1',
+			'/([^us])s$/'		=> '\1'
+		);
+
+		foreach ($singular_rules as $rule => $replacement)
+		{
+			if (preg_match($rule, $result))
+			{
+				$result = preg_replace($rule, $replacement, $result);
+				break;
+			}
+		}
+
+		return $result;
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('plural'))
+{
+	/**
+	 * Plural
+	 *
+	 * Takes a singular word and makes it plural
+	 *
+	 * @param	string	$str	Input string
+	 * @return	string
+	 */
+	function plural($str)
+	{
+		$result = strval($str);
+
+		if ( ! word_is_countable($result))
+		{
+			return $result;
+		}
+
+		$plural_rules = array(
+			'/(quiz)$/'                => '\1zes',      // quizzes
+			'/^(ox)$/'                 => '\1\2en',     // ox
+			'/([m|l])ouse$/'           => '\1ice',      // mouse, louse
+			'/(matr|vert|ind)ix|ex$/'  => '\1ices',     // matrix, vertex, index
+			'/(x|ch|ss|sh)$/'          => '\1es',       // search, switch, fix, box, process, address
+			'/([^aeiouy]|qu)y$/'       => '\1ies',      // query, ability, agency
+			'/(hive)$/'                => '\1s',        // archive, hive
+			'/(?:([^f])fe|([lr])f)$/'  => '\1\2ves',    // half, safe, wife
+			'/sis$/'                   => 'ses',        // basis, diagnosis
+			'/([ti])um$/'              => '\1a',        // datum, medium
+			'/(p)erson$/'              => '\1eople',    // person, salesperson
+			'/(m)an$/'                 => '\1en',       // man, woman, spokesman
+			'/(c)hild$/'               => '\1hildren',  // child
+			'/(buffal|tomat)o$/'       => '\1\2oes',    // buffalo, tomato
+			'/(bu|campu)s$/'           => '\1\2ses',    // bus, campus
+			'/(alias|status|virus)$/'  => '\1es',       // alias
+			'/(octop)us$/'             => '\1i',        // octopus
+			'/(ax|cris|test)is$/'      => '\1es',       // axis, crisis
+			'/s$/'                     => 's',          // no change (compatibility)
+			'/$/'                      => 's',
+		);
+
+		foreach ($plural_rules as $rule => $replacement)
+		{
+			if (preg_match($rule, $result))
+			{
+				$result = preg_replace($rule, $replacement, $result);
+				break;
+			}
+		}
+
+		return $result;
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('camelize'))
+{
+	/**
+	 * Camelize
+	 *
+	 * Takes multiple words separated by spaces or underscores and camelizes them
+	 *
+	 * @param	string	$str	Input string
+	 * @return	string
+	 */
+	function camelize($str)
+	{
+		return strtolower($str[0]).substr(str_replace(' ', '', ucwords(preg_replace('/[\s_]+/', ' ', $str))), 1);
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('underscore'))
+{
+	/**
+	 * Underscore
+	 *
+	 * Takes multiple words separated by spaces and underscores them
+	 *
+	 * @param	string	$str	Input string
+	 * @return	string
+	 */
+	function underscore($str)
+	{
+		return preg_replace('/[\s]+/', '_', trim(MB_ENABLED ? mb_strtolower($str) : strtolower($str)));
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('humanize'))
+{
+	/**
+	 * Humanize
+	 *
+	 * Takes multiple words separated by the separator and changes them to spaces
+	 *
+	 * @param	string	$str		Input string
+	 * @param 	string	$separator	Input separator
+	 * @return	string
+	 */
+	function humanize($str, $separator = '_')
+	{
+		return ucwords(preg_replace('/['.preg_quote($separator).']+/', ' ', trim(MB_ENABLED ? mb_strtolower($str) : strtolower($str))));
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('word_is_countable'))
+{
+	/**
+	 * Checks if the given word has a plural version.
+	 *
+	 * @param	string	$word	Word to check
+	 * @return	bool
+	 */
+	function word_is_countable($word)
+	{
+		return ! in_array(
+			strtolower($word),
+			array(
+				'audio',
+				'bison',
+				'chassis',
+				'compensation',
+				'coreopsis',
+				'data',
+				'deer',
+				'education',
+				'emoji',
+				'equipment',
+				'fish',
+				'furniture',
+				'gold',
+				'information',
+				'knowledge',
+				'love',
+				'rain',
+				'money',
+				'moose',
+				'nutrition',
+				'offspring',
+				'plankton',
+				'pokemon',
+				'police',
+				'rice',
+				'series',
+				'sheep',
+				'species',
+				'swine',
+				'traffic',
+				'wheat'
+			)
+		);
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('is_countable'))
+{
+	function is_countable($word)
+	{
+		trigger_error('is_countable() is a native PHP function since version 7.3.0; use word_is_countable() instead', E_USER_WARNING);
+		return word_is_countable($word);
+	}
+}
diff --git a/system/helpers/language_helper.php b/system/helpers/language_helper.php
new file mode 100644
index 0000000..d6cc1c1
--- /dev/null
+++ b/system/helpers/language_helper.php
@@ -0,0 +1,76 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Language Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/language_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('lang'))
+{
+	/**
+	 * Lang
+	 *
+	 * Fetches a language variable and optionally outputs a form label
+	 *
+	 * @param	string	$line		The language line
+	 * @param	string	$for		The "for" value (id of the form element)
+	 * @param	array	$attributes	Any additional HTML attributes
+	 * @return	string
+	 */
+	function lang($line, $for = '', $attributes = array())
+	{
+		$line = get_instance()->lang->line($line);
+
+		if ($for !== '')
+		{
+			$line = '<label for="'.$for.'"'._stringify_attributes($attributes).'>'.$line.'</label>';
+		}
+
+		return $line;
+	}
+}
diff --git a/system/helpers/number_helper.php b/system/helpers/number_helper.php
new file mode 100644
index 0000000..27ad80f
--- /dev/null
+++ b/system/helpers/number_helper.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Number Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/number_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('byte_format'))
+{
+	/**
+	 * Formats a numbers as bytes, based on size, and adds the appropriate suffix
+	 *
+	 * @param	mixed	will be cast as int
+	 * @param	int
+	 * @return	string
+	 */
+	function byte_format($num, $precision = 1)
+	{
+		$CI =& get_instance();
+		$CI->lang->load('number');
+
+		if ($num >= 1000000000000)
+		{
+			$num = round($num / 1099511627776, $precision);
+			$unit = $CI->lang->line('terabyte_abbr');
+		}
+		elseif ($num >= 1000000000)
+		{
+			$num = round($num / 1073741824, $precision);
+			$unit = $CI->lang->line('gigabyte_abbr');
+		}
+		elseif ($num >= 1000000)
+		{
+			$num = round($num / 1048576, $precision);
+			$unit = $CI->lang->line('megabyte_abbr');
+		}
+		elseif ($num >= 1000)
+		{
+			$num = round($num / 1024, $precision);
+			$unit = $CI->lang->line('kilobyte_abbr');
+		}
+		else
+		{
+			$unit = $CI->lang->line('bytes');
+			return number_format($num).' '.$unit;
+		}
+
+		return number_format($num, $precision).' '.$unit;
+	}
+}
diff --git a/system/helpers/path_helper.php b/system/helpers/path_helper.php
new file mode 100644
index 0000000..a8f7823
--- /dev/null
+++ b/system/helpers/path_helper.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Path Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/path_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('set_realpath'))
+{
+	/**
+	 * Set Realpath
+	 *
+	 * @param	string
+	 * @param	bool	checks to see if the path exists
+	 * @return	string
+	 */
+	function set_realpath($path, $check_existance = FALSE)
+	{
+		// Security check to make sure the path is NOT a URL. No remote file inclusion!
+		if (preg_match('#^(http:\/\/|https:\/\/|www\.|ftp|php:\/\/)#i', $path) OR filter_var($path, FILTER_VALIDATE_IP) === $path)
+		{
+			show_error('The path you submitted must be a local server path, not a URL');
+		}
+
+		// Resolve the path
+		if (realpath($path) !== FALSE)
+		{
+			$path = realpath($path);
+		}
+		elseif ($check_existance && ! is_dir($path) && ! is_file($path))
+		{
+			show_error('Not a valid path: '.$path);
+		}
+
+		// Add a trailing slash, if this is a directory
+		return is_dir($path) ? rtrim($path, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR : $path;
+	}
+}
diff --git a/system/helpers/security_helper.php b/system/helpers/security_helper.php
new file mode 100644
index 0000000..dc2b1a4
--- /dev/null
+++ b/system/helpers/security_helper.php
@@ -0,0 +1,138 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Security Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/security_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('xss_clean'))
+{
+	/**
+	 * XSS Filtering
+	 *
+	 * @param	string
+	 * @param	bool	whether or not the content is an image file
+	 * @return	string
+	 */
+	function xss_clean($str, $is_image = FALSE)
+	{
+		return get_instance()->security->xss_clean($str, $is_image);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('sanitize_filename'))
+{
+	/**
+	 * Sanitize Filename
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	function sanitize_filename($filename)
+	{
+		return get_instance()->security->sanitize_filename($filename);
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('do_hash'))
+{
+	/**
+	 * Hash encode a string
+	 *
+	 * @todo	Remove in version 3.1+.
+	 * @deprecated	3.0.0	Use PHP's native hash() instead.
+	 * @param	string	$str
+	 * @param	string	$type = 'sha1'
+	 * @return	string
+	 */
+	function do_hash($str, $type = 'sha1')
+	{
+		if ( ! in_array(strtolower($type), hash_algos()))
+		{
+			$type = 'md5';
+		}
+
+		return hash($type, $str);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('strip_image_tags'))
+{
+	/**
+	 * Strip Image Tags
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	function strip_image_tags($str)
+	{
+		return get_instance()->security->strip_image_tags($str);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('encode_php_tags'))
+{
+	/**
+	 * Convert PHP tags to entities
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	function encode_php_tags($str)
+	{
+		return str_replace(array('<?', '?>'), array('&lt;?', '?&gt;'), $str);
+	}
+}
diff --git a/system/helpers/smiley_helper.php b/system/helpers/smiley_helper.php
new file mode 100644
index 0000000..091e6de
--- /dev/null
+++ b/system/helpers/smiley_helper.php
@@ -0,0 +1,256 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Smiley Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/smiley_helper.html
+ * @deprecated	3.0.0	This helper is too specific for CI.
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('smiley_js'))
+{
+	/**
+	 * Smiley Javascript
+	 *
+	 * Returns the javascript required for the smiley insertion.  Optionally takes
+	 * an array of aliases to loosely couple the smiley array to the view.
+	 *
+	 * @param	mixed	alias name or array of alias->field_id pairs
+	 * @param	string	field_id if alias name was passed in
+	 * @param	bool
+	 * @return	array
+	 */
+	function smiley_js($alias = '', $field_id = '', $inline = TRUE)
+	{
+		static $do_setup = TRUE;
+		$r = '';
+
+		if ($alias !== '' && ! is_array($alias))
+		{
+			$alias = array($alias => $field_id);
+		}
+
+		if ($do_setup === TRUE)
+		{
+			$do_setup = FALSE;
+			$m = array();
+
+			if (is_array($alias))
+			{
+				foreach ($alias as $name => $id)
+				{
+					$m[] = '"'.$name.'" : "'.$id.'"';
+				}
+			}
+
+			$m = '{'.implode(',', $m).'}';
+
+			$r .= <<<EOF
+			var smiley_map = {$m};
+
+			function insert_smiley(smiley, field_id) {
+				var el = document.getElementById(field_id), newStart;
+
+				if ( ! el && smiley_map[field_id]) {
+					el = document.getElementById(smiley_map[field_id]);
+
+					if ( ! el)
+						return false;
+				}
+
+				el.focus();
+				smiley = " " + smiley;
+
+				if ('selectionStart' in el) {
+					newStart = el.selectionStart + smiley.length;
+
+					el.value = el.value.substr(0, el.selectionStart) +
+									smiley +
+									el.value.substr(el.selectionEnd, el.value.length);
+					el.setSelectionRange(newStart, newStart);
+				}
+				else if (document.selection) {
+					document.selection.createRange().text = smiley;
+				}
+			}
+EOF;
+		}
+		elseif (is_array($alias))
+		{
+			foreach ($alias as $name => $id)
+			{
+				$r .= 'smiley_map["'.$name.'"] = "'.$id."\";\n";
+			}
+		}
+
+		return ($inline)
+			? '<script type="text/javascript" charset="utf-8">/*<![CDATA[ */'.$r.'// ]]></script>'
+			: $r;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('get_clickable_smileys'))
+{
+	/**
+	 * Get Clickable Smileys
+	 *
+	 * Returns an array of image tag links that can be clicked to be inserted
+	 * into a form field.
+	 *
+	 * @param	string	the URL to the folder containing the smiley images
+	 * @param	array
+	 * @return	array
+	 */
+	function get_clickable_smileys($image_url, $alias = '')
+	{
+		// For backward compatibility with js_insert_smiley
+		if (is_array($alias))
+		{
+			$smileys = $alias;
+		}
+		elseif (FALSE === ($smileys = _get_smiley_array()))
+		{
+			return FALSE;
+		}
+
+		// Add a trailing slash to the file path if needed
+		$image_url = rtrim($image_url, '/').'/';
+
+		$used = array();
+		foreach ($smileys as $key => $val)
+		{
+			// Keep duplicates from being used, which can happen if the
+			// mapping array contains multiple identical replacements. For example:
+			// :-) and :) might be replaced with the same image so both smileys
+			// will be in the array.
+			if (isset($used[$smileys[$key][0]]))
+			{
+				continue;
+			}
+
+			$link[] = '<a href="javascript:void(0);" onclick="insert_smiley(\''.$key.'\', \''.$alias.'\')"><img src="'.$image_url.$smileys[$key][0].'" alt="'.$smileys[$key][3].'" style="width: '.$smileys[$key][1].'; height: '.$smileys[$key][2].'; border: 0;" /></a>';
+			$used[$smileys[$key][0]] = TRUE;
+		}
+
+		return $link;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('parse_smileys'))
+{
+	/**
+	 * Parse Smileys
+	 *
+	 * Takes a string as input and swaps any contained smileys for the actual image
+	 *
+	 * @param	string	the text to be parsed
+	 * @param	string	the URL to the folder containing the smiley images
+	 * @param	array
+	 * @return	string
+	 */
+	function parse_smileys($str = '', $image_url = '', $smileys = NULL)
+	{
+		if ($image_url === '' OR ( ! is_array($smileys) && FALSE === ($smileys = _get_smiley_array())))
+		{
+			return $str;
+		}
+
+		// Add a trailing slash to the file path if needed
+		$image_url = rtrim($image_url, '/').'/';
+
+		foreach ($smileys as $key => $val)
+		{
+			$str = str_replace($key, '<img src="'.$image_url.$smileys[$key][0].'" alt="'.$smileys[$key][3].'" style="width: '.$smileys[$key][1].'; height: '.$smileys[$key][2].'; border: 0;" />', $str);
+		}
+
+		return $str;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('_get_smiley_array'))
+{
+	/**
+	 * Get Smiley Array
+	 *
+	 * Fetches the config/smiley.php file
+	 *
+	 * @return	mixed
+	 */
+	function _get_smiley_array()
+	{
+		static $_smileys;
+
+		if ( ! is_array($_smileys))
+		{
+			if (file_exists(APPPATH.'config/smileys.php'))
+			{
+				include(APPPATH.'config/smileys.php');
+			}
+
+			if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/smileys.php'))
+			{
+				include(APPPATH.'config/'.ENVIRONMENT.'/smileys.php');
+			}
+
+			if (empty($smileys) OR ! is_array($smileys))
+			{
+				$_smileys = array();
+				return FALSE;
+			}
+
+			$_smileys = $smileys;
+		}
+
+		return $_smileys;
+	}
+}
diff --git a/system/helpers/string_helper.php b/system/helpers/string_helper.php
new file mode 100644
index 0000000..7370f39
--- /dev/null
+++ b/system/helpers/string_helper.php
@@ -0,0 +1,305 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter String Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/string_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('trim_slashes'))
+{
+	/**
+	 * Trim Slashes
+	 *
+	 * Removes any leading/trailing slashes from a string:
+	 *
+	 * /this/that/theother/
+	 *
+	 * becomes:
+	 *
+	 * this/that/theother
+	 *
+	 * @todo	Remove in version 3.1+.
+	 * @deprecated	3.0.0	This is just an alias for PHP's native trim()
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	function trim_slashes($str)
+	{
+		return trim($str, '/');
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('strip_slashes'))
+{
+	/**
+	 * Strip Slashes
+	 *
+	 * Removes slashes contained in a string or in an array
+	 *
+	 * @param	mixed	string or array
+	 * @return	mixed	string or array
+	 */
+	function strip_slashes($str)
+	{
+		if ( ! is_array($str))
+		{
+			return stripslashes($str);
+		}
+
+		foreach ($str as $key => $val)
+		{
+			$str[$key] = strip_slashes($val);
+		}
+
+		return $str;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('strip_quotes'))
+{
+	/**
+	 * Strip Quotes
+	 *
+	 * Removes single and double quotes from a string
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	function strip_quotes($str)
+	{
+		return str_replace(array('"', "'"), '', $str);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('quotes_to_entities'))
+{
+	/**
+	 * Quotes to Entities
+	 *
+	 * Converts single and double quotes to entities
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	function quotes_to_entities($str)
+	{
+		return str_replace(array("\'","\"","'",'"'), array("&#39;","&quot;","&#39;","&quot;"), $str);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('reduce_double_slashes'))
+{
+	/**
+	 * Reduce Double Slashes
+	 *
+	 * Converts double slashes in a string to a single slash,
+	 * except those found in http://
+	 *
+	 * http://www.some-site.com//index.php
+	 *
+	 * becomes:
+	 *
+	 * http://www.some-site.com/index.php
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	function reduce_double_slashes($str)
+	{
+		return preg_replace('#(^|[^:])//+#', '\\1/', $str);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('reduce_multiples'))
+{
+	/**
+	 * Reduce Multiples
+	 *
+	 * Reduces multiple instances of a particular character.  Example:
+	 *
+	 * Fred, Bill,, Joe, Jimmy
+	 *
+	 * becomes:
+	 *
+	 * Fred, Bill, Joe, Jimmy
+	 *
+	 * @param	string
+	 * @param	string	the character you wish to reduce
+	 * @param	bool	TRUE/FALSE - whether to trim the character from the beginning/end
+	 * @return	string
+	 */
+	function reduce_multiples($str, $character = ',', $trim = FALSE)
+	{
+		$str = preg_replace('#'.preg_quote($character, '#').'{2,}#', $character, $str);
+		return ($trim === TRUE) ? trim($str, $character) : $str;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('random_string'))
+{
+	/**
+	 * Create a "Random" String
+	 *
+	 * @param	string	type of random string.  basic, alpha, alnum, numeric, nozero, unique, md5, encrypt and sha1
+	 * @param	int	number of characters
+	 * @return	string
+	 */
+	function random_string($type = 'alnum', $len = 8)
+	{
+		switch ($type)
+		{
+			case 'basic':
+				return mt_rand();
+			case 'alnum':
+			case 'numeric':
+			case 'nozero':
+			case 'alpha':
+				switch ($type)
+				{
+					case 'alpha':
+						$pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+						break;
+					case 'alnum':
+						$pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+						break;
+					case 'numeric':
+						$pool = '0123456789';
+						break;
+					case 'nozero':
+						$pool = '123456789';
+						break;
+				}
+				return substr(str_shuffle(str_repeat($pool, ceil($len / strlen($pool)))), 0, $len);
+			case 'unique': // todo: remove in 3.1+
+			case 'md5':
+				return md5(uniqid(mt_rand()));
+			case 'encrypt': // todo: remove in 3.1+
+			case 'sha1':
+				return sha1(uniqid(mt_rand(), TRUE));
+		}
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('increment_string'))
+{
+	/**
+	 * Add's _1 to a string or increment the ending number to allow _2, _3, etc
+	 *
+	 * @param	string	required
+	 * @param	string	What should the duplicate number be appended with
+	 * @param	string	Which number should be used for the first dupe increment
+	 * @return	string
+	 */
+	function increment_string($str, $separator = '_', $first = 1)
+	{
+		preg_match('/(.+)'.preg_quote($separator, '/').'([0-9]+)$/', $str, $match);
+		return isset($match[2]) ? $match[1].$separator.($match[2] + 1) : $str.$separator.$first;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('alternator'))
+{
+	/**
+	 * Alternator
+	 *
+	 * Allows strings to be alternated. See docs...
+	 *
+	 * @param	string (as many parameters as needed)
+	 * @return	string
+	 */
+	function alternator()
+	{
+		static $i;
+
+		if (func_num_args() === 0)
+		{
+			$i = 0;
+			return '';
+		}
+
+		$args = func_get_args();
+		return $args[($i++ % count($args))];
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('repeater'))
+{
+	/**
+	 * Repeater function
+	 *
+	 * @todo	Remove in version 3.1+.
+	 * @deprecated	3.0.0	This is just an alias for PHP's native str_repeat()
+	 *
+	 * @param	string	$data	String to repeat
+	 * @param	int	$num	Number of repeats
+	 * @return	string
+	 */
+	function repeater($data, $num = 1)
+	{
+		return ($num > 0) ? str_repeat($data, $num) : '';
+	}
+}
diff --git a/system/helpers/text_helper.php b/system/helpers/text_helper.php
new file mode 100644
index 0000000..506d45a
--- /dev/null
+++ b/system/helpers/text_helper.php
@@ -0,0 +1,568 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Text Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/text_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('word_limiter'))
+{
+	/**
+	 * Word Limiter
+	 *
+	 * Limits a string to X number of words.
+	 *
+	 * @param	string
+	 * @param	int
+	 * @param	string	the end character. Usually an ellipsis
+	 * @return	string
+	 */
+	function word_limiter($str, $limit = 100, $end_char = '&#8230;')
+	{
+		if (trim($str) === '')
+		{
+			return $str;
+		}
+
+		preg_match('/^\s*+(?:\S++\s*+){1,'.(int) $limit.'}/', $str, $matches);
+
+		if (strlen($str) === strlen($matches[0]))
+		{
+			$end_char = '';
+		}
+
+		return rtrim($matches[0]).$end_char;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('character_limiter'))
+{
+	/**
+	 * Character Limiter
+	 *
+	 * Limits the string based on the character count.  Preserves complete words
+	 * so the character count may not be exactly as specified.
+	 *
+	 * @param	string
+	 * @param	int
+	 * @param	string	the end character. Usually an ellipsis
+	 * @return	string
+	 */
+	function character_limiter($str, $n = 500, $end_char = '&#8230;')
+	{
+		if (mb_strlen($str) < $n)
+		{
+			return $str;
+		}
+
+		// a bit complicated, but faster than preg_replace with \s+
+		$str = preg_replace('/ {2,}/', ' ', str_replace(array("\r", "\n", "\t", "\v", "\f"), ' ', $str));
+
+		if (mb_strlen($str) <= $n)
+		{
+			return $str;
+		}
+
+		$out = '';
+		foreach (explode(' ', trim($str)) as $val)
+		{
+			$out .= $val.' ';
+
+			if (mb_strlen($out) >= $n)
+			{
+				$out = trim($out);
+				return (mb_strlen($out) === mb_strlen($str)) ? $out : $out.$end_char;
+			}
+		}
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('ascii_to_entities'))
+{
+	/**
+	 * High ASCII to Entities
+	 *
+	 * Converts high ASCII text and MS Word special characters to character entities
+	 *
+	 * @param	string	$str
+	 * @return	string
+	 */
+	function ascii_to_entities($str)
+	{
+		$out = '';
+		$length = defined('MB_OVERLOAD_STRING')
+			? mb_strlen($str, '8bit') - 1
+			: strlen($str) - 1;
+		for ($i = 0, $count = 1, $temp = array(); $i <= $length; $i++)
+		{
+			$ordinal = ord($str[$i]);
+
+			if ($ordinal < 128)
+			{
+				/*
+					If the $temp array has a value but we have moved on, then it seems only
+					fair that we output that entity and restart $temp before continuing. -Paul
+				*/
+				if (count($temp) === 1)
+				{
+					$out .= '&#'.array_shift($temp).';';
+					$count = 1;
+				}
+
+				$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();
+				}
+				// If this is the last iteration, just output whatever we have
+				elseif ($i === $length)
+				{
+					$out .= '&#'.implode(';', $temp).';';
+				}
+			}
+		}
+
+		return $out;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('entities_to_ascii'))
+{
+	/**
+	 * Entities to ASCII
+	 *
+	 * Converts character entities back to ASCII
+	 *
+	 * @param	string
+	 * @param	bool
+	 * @return	string
+	 */
+	function entities_to_ascii($str, $all = TRUE)
+	{
+		if (preg_match_all('/\&#(\d+)\;/', $str, $matches))
+		{
+			for ($i = 0, $s = count($matches[0]); $i < $s; $i++)
+			{
+				$digits = $matches[1][$i];
+				$out = '';
+
+				if ($digits < 128)
+				{
+					$out .= chr($digits);
+
+				}
+				elseif ($digits < 2048)
+				{
+					$out .= chr(192 + (($digits - ($digits % 64)) / 64)).chr(128 + ($digits % 64));
+				}
+				else
+				{
+					$out .= chr(224 + (($digits - ($digits % 4096)) / 4096))
+						.chr(128 + ((($digits % 4096) - ($digits % 64)) / 64))
+						.chr(128 + ($digits % 64));
+				}
+
+				$str = str_replace($matches[0][$i], $out, $str);
+			}
+		}
+
+		if ($all)
+		{
+			return str_replace(
+				array('&amp;', '&lt;', '&gt;', '&quot;', '&apos;', '&#45;'),
+				array('&', '<', '>', '"', "'", '-'),
+				$str
+			);
+		}
+
+		return $str;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('word_censor'))
+{
+	/**
+	 * Word Censoring Function
+	 *
+	 * Supply a string and an array of disallowed words and any
+	 * matched words will be converted to #### or to the replacement
+	 * word you've submitted.
+	 *
+	 * @param	string	the text string
+	 * @param	string	the array of censored words
+	 * @param	string	the optional replacement value
+	 * @return	string
+	 */
+	function word_censor($str, $censored, $replacement = '')
+	{
+		if ( ! is_array($censored))
+		{
+			return $str;
+		}
+
+		$str = ' '.$str.' ';
+
+		// \w, \b and a few others do not match on a unicode character
+		// set for performance reasons. As a result words like über
+		// will not match on a word boundary. Instead, we'll assume that
+		// a bad word will be bookeneded by any of these characters.
+		$delim = '[-_\'\"`(){}<>\[\]|!?@#%&,.:;^~*+=\/ 0-9\n\r\t]';
+
+		foreach ($censored as $badword)
+		{
+			$badword = str_replace('\*', '\w*?', preg_quote($badword, '/'));
+			if ($replacement !== '')
+			{
+				$str = preg_replace(
+					"/({$delim})(".$badword.")({$delim})/i",
+					"\\1{$replacement}\\3",
+					$str
+				);
+			}
+			elseif (preg_match_all("/{$delim}(".$badword."){$delim}/i", $str, $matches, PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE))
+			{
+				$matches = $matches[1];
+				for ($i = count($matches) - 1; $i >= 0; $i--)
+				{
+					$length = strlen($matches[$i][0]);
+					$str = substr_replace(
+						$str,
+						str_repeat('#', $length),
+						$matches[$i][1],
+						$length
+					);
+				}
+			}
+		}
+
+		return trim($str);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('highlight_code'))
+{
+	/**
+	 * Code Highlighter
+	 *
+	 * Colorizes code strings
+	 *
+	 * @param	string	the text string
+	 * @return	string
+	 */
+	function highlight_code($str)
+	{
+		/* The highlight string function encodes and highlights
+		 * brackets so we need them to start raw.
+		 *
+		 * Also replace any existing PHP tags to temporary markers
+		 * so they don't accidentally break the string out of PHP,
+		 * and thus, thwart the highlighting.
+		 */
+		$str = str_replace(
+			array('&lt;', '&gt;', '<?', '?>', '<%', '%>', '\\', '</script>'),
+			array('<', '>', 'phptagopen', 'phptagclose', 'asptagopen', 'asptagclose', 'backslashtmp', 'scriptclose'),
+			$str
+		);
+
+		// The highlight_string function requires that the text be surrounded
+		// by PHP tags, which we will remove later
+		$str = highlight_string('<?php '.$str.' ?>', TRUE);
+
+		// Remove our artificially added PHP, and the syntax highlighting that came with it
+		$str = preg_replace(
+			array(
+				'/<span style="color: #([A-Z0-9]+)">&lt;\?php(&nbsp;| )/i',
+				'/(<span style="color: #[A-Z0-9]+">.*?)\?&gt;<\/span>\n<\/span>\n<\/code>/is',
+				'/<span style="color: #[A-Z0-9]+"\><\/span>/i'
+			),
+			array(
+				'<span style="color: #$1">',
+				"$1</span>\n</span>\n</code>",
+				''
+			),
+			$str
+		);
+
+		// Replace our markers back to PHP tags.
+		return str_replace(
+			array('phptagopen', 'phptagclose', 'asptagopen', 'asptagclose', 'backslashtmp', 'scriptclose'),
+			array('&lt;?', '?&gt;', '&lt;%', '%&gt;', '\\', '&lt;/script&gt;'),
+			$str
+		);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('highlight_phrase'))
+{
+	/**
+	 * Phrase Highlighter
+	 *
+	 * Highlights a phrase within a text string
+	 *
+	 * @param	string	$str		the text string
+	 * @param	string	$phrase		the phrase you'd like to highlight
+	 * @param	string	$tag_open	the openging tag to precede the phrase with
+	 * @param	string	$tag_close	the closing tag to end the phrase with
+	 * @return	string
+	 */
+	function highlight_phrase($str, $phrase, $tag_open = '<mark>', $tag_close = '</mark>')
+	{
+		return ($str !== '' && $phrase !== '')
+			? preg_replace('/('.preg_quote($phrase, '/').')/i'.(UTF8_ENABLED ? 'u' : ''), $tag_open.'\\1'.$tag_close, $str)
+			: $str;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('convert_accented_characters'))
+{
+	/**
+	 * Convert Accented Foreign Characters to ASCII
+	 *
+	 * @param	string	$str	Input string
+	 * @return	string
+	 */
+	function convert_accented_characters($str)
+	{
+		static $array_from, $array_to;
+
+		if ( ! is_array($array_from))
+		{
+			if (file_exists(APPPATH.'config/foreign_chars.php'))
+			{
+				include(APPPATH.'config/foreign_chars.php');
+			}
+
+			if (file_exists(APPPATH.'config/'.ENVIRONMENT.'/foreign_chars.php'))
+			{
+				include(APPPATH.'config/'.ENVIRONMENT.'/foreign_chars.php');
+			}
+
+			if (empty($foreign_characters) OR ! is_array($foreign_characters))
+			{
+				$array_from = array();
+				$array_to = array();
+
+				return $str;
+			}
+
+			$array_from = array_keys($foreign_characters);
+			$array_to = array_values($foreign_characters);
+		}
+
+		return preg_replace($array_from, $array_to, $str);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('word_wrap'))
+{
+	/**
+	 * Word Wrap
+	 *
+	 * Wraps text at the specified character. Maintains the integrity of words.
+	 * Anything placed between {unwrap}{/unwrap} will not be word wrapped, nor
+	 * will URLs.
+	 *
+	 * @param	string	$str		the text string
+	 * @param	int	$charlim = 76	the number of characters to wrap at
+	 * @return	string
+	 */
+	function word_wrap($str, $charlim = 76)
+	{
+		// Set the character limit
+		is_numeric($charlim) OR $charlim = 76;
+
+		// Reduce multiple spaces
+		$str = preg_replace('| +|', ' ', $str);
+
+		// Standardize newlines
+		if (strpos($str, "\r") !== FALSE)
+		{
+			$str = str_replace(array("\r\n", "\r"), "\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."\n";
+				continue;
+			}
+
+			$temp = '';
+			while (mb_strlen($line) > $charlim)
+			{
+				// 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);
+			}
+
+			// 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."\n".$line."\n";
+			}
+			else
+			{
+				$output .= $line."\n";
+			}
+		}
+
+		// Put our markers back
+		if (count($unwrap) > 0)
+		{
+			foreach ($unwrap as $key => $val)
+			{
+				$output = str_replace('{{unwrapped'.$key.'}}', $val, $output);
+			}
+		}
+
+		return $output;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('ellipsize'))
+{
+	/**
+	 * Ellipsize String
+	 *
+	 * This function will strip tags from a string, split it at its max_length and ellipsize
+	 *
+	 * @param	string	string to ellipsize
+	 * @param	int	max length of string
+	 * @param	mixed	int (1|0) or float, .5, .2, etc for position to split
+	 * @param	string	ellipsis ; Default '...'
+	 * @return	string	ellipsized string
+	 */
+	function ellipsize($str, $max_length, $position = 1, $ellipsis = '&hellip;')
+	{
+		// Strip tags
+		$str = trim(strip_tags($str));
+
+		// Is the string long enough to ellipsize?
+		if (mb_strlen($str) <= $max_length)
+		{
+			return $str;
+		}
+
+		$beg = mb_substr($str, 0, floor($max_length * $position));
+		$position = ($position > 1) ? 1 : $position;
+
+		if ($position === 1)
+		{
+			$end = mb_substr($str, 0, -($max_length - mb_strlen($beg)));
+		}
+		else
+		{
+			$end = mb_substr($str, -($max_length - mb_strlen($beg)));
+		}
+
+		return $beg.$ellipsis.$end;
+	}
+}
diff --git a/system/helpers/typography_helper.php b/system/helpers/typography_helper.php
new file mode 100644
index 0000000..d51de08
--- /dev/null
+++ b/system/helpers/typography_helper.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter Typography Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/typography_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('nl2br_except_pre'))
+{
+	/**
+	 * Convert newlines to HTML line breaks except within PRE tags
+	 *
+	 * @param	string
+	 * @return	string
+	 */
+	function nl2br_except_pre($str)
+	{
+		$CI =& get_instance();
+		$CI->load->library('typography');
+		return $CI->typography->nl2br_except_pre($str);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('auto_typography'))
+{
+	/**
+	 * Auto Typography Wrapper Function
+	 *
+	 * @param	string	$str
+	 * @param	bool	$reduce_linebreaks = FALSE	whether to reduce multiple instances of double newlines to two
+	 * @return	string
+	 */
+	function auto_typography($str, $reduce_linebreaks = FALSE)
+	{
+		$CI =& get_instance();
+		$CI->load->library('typography');
+		return $CI->typography->auto_typography($str, $reduce_linebreaks);
+	}
+}
+
+// --------------------------------------------------------------------
+
+if ( ! function_exists('entity_decode'))
+{
+	/**
+	 * HTML Entities Decode
+	 *
+	 * This function is a replacement for html_entity_decode()
+	 *
+	 * @param	string
+	 * @param	string
+	 * @return	string
+	 */
+	function entity_decode($str, $charset = NULL)
+	{
+		return get_instance()->security->entity_decode($str, $charset);
+	}
+}
diff --git a/system/helpers/url_helper.php b/system/helpers/url_helper.php
new file mode 100644
index 0000000..d1d7ec1
--- /dev/null
+++ b/system/helpers/url_helper.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) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter URL Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/url_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('site_url'))
+{
+	/**
+	 * Site URL
+	 *
+	 * Create a local URL based on your basepath. Segments can be passed via the
+	 * first parameter either as a string or an array.
+	 *
+	 * @param	string	$uri
+	 * @param	string	$protocol
+	 * @return	string
+	 */
+	function site_url($uri = '', $protocol = NULL)
+	{
+		return get_instance()->config->site_url($uri, $protocol);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('base_url'))
+{
+	/**
+	 * Base URL
+	 *
+	 * Create a local URL based on your basepath.
+	 * Segments can be passed in as a string or an array, same as site_url
+	 * or a URL to a file can be passed in, e.g. to an image file.
+	 *
+	 * @param	string	$uri
+	 * @param	string	$protocol
+	 * @return	string
+	 */
+	function base_url($uri = '', $protocol = NULL)
+	{
+		return get_instance()->config->base_url($uri, $protocol);
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('current_url'))
+{
+	/**
+	 * Current URL
+	 *
+	 * Returns the full URL (including segments) of the page where this
+	 * function is placed
+	 *
+	 * @return	string
+	 */
+	function current_url()
+	{
+		$CI =& get_instance();
+		return $CI->config->site_url($CI->uri->uri_string());
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('uri_string'))
+{
+	/**
+	 * URL String
+	 *
+	 * Returns the URI segments.
+	 *
+	 * @return	string
+	 */
+	function uri_string()
+	{
+		return get_instance()->uri->uri_string();
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('index_page'))
+{
+	/**
+	 * Index page
+	 *
+	 * Returns the "index_page" from your config file
+	 *
+	 * @return	string
+	 */
+	function index_page()
+	{
+		return get_instance()->config->item('index_page');
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('anchor'))
+{
+	/**
+	 * Anchor Link
+	 *
+	 * Creates an anchor based on the local URL.
+	 *
+	 * @param	string	the URL
+	 * @param	string	the link title
+	 * @param	mixed	any attributes
+	 * @return	string
+	 */
+	function anchor($uri = '', $title = '', $attributes = '')
+	{
+		$title = (string) $title;
+
+		$site_url = is_array($uri)
+			? site_url($uri)
+			: (preg_match('#^(\w+:)?//#i', $uri) ? $uri : site_url($uri));
+
+		if ($title === '')
+		{
+			$title = $site_url;
+		}
+
+		if ($attributes !== '')
+		{
+			$attributes = _stringify_attributes($attributes);
+		}
+
+		return '<a href="'.$site_url.'"'.$attributes.'>'.$title.'</a>';
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('anchor_popup'))
+{
+	/**
+	 * Anchor Link - Pop-up version
+	 *
+	 * Creates an anchor based on the local URL. The link
+	 * opens a new window based on the attributes specified.
+	 *
+	 * @param	string	the URL
+	 * @param	string	the link title
+	 * @param	mixed	any attributes
+	 * @return	string
+	 */
+	function anchor_popup($uri = '', $title = '', $attributes = FALSE)
+	{
+		$title = (string) $title;
+		$site_url = preg_match('#^(\w+:)?//#i', $uri) ? $uri : site_url($uri);
+
+		if ($title === '')
+		{
+			$title = $site_url;
+		}
+
+		if ($attributes === FALSE)
+		{
+			return '<a href="'.$site_url.'" onclick="window.open(\''.$site_url."', '_blank'); return false;\">".$title.'</a>';
+		}
+
+		if ( ! is_array($attributes))
+		{
+			$attributes = array($attributes);
+
+			// Ref: http://www.w3schools.com/jsref/met_win_open.asp
+			$window_name = '_blank';
+		}
+		elseif ( ! empty($attributes['window_name']))
+		{
+			$window_name = $attributes['window_name'];
+			unset($attributes['window_name']);
+		}
+		else
+		{
+			$window_name = '_blank';
+		}
+
+		foreach (array('width' => '800', 'height' => '600', 'scrollbars' => 'yes', 'menubar' => 'no', 'status' => 'yes', 'resizable' => 'yes', 'screenx' => '0', 'screeny' => '0') as $key => $val)
+		{
+			$atts[$key] = isset($attributes[$key]) ? $attributes[$key] : $val;
+			unset($attributes[$key]);
+		}
+
+		$attributes = _stringify_attributes($attributes);
+
+		return '<a href="'.$site_url
+			.'" onclick="window.open(\''.$site_url."', '".$window_name."', '"._stringify_attributes($atts, TRUE)."'); return false;\""
+			.$attributes.'>'.$title.'</a>';
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('mailto'))
+{
+	/**
+	 * Mailto Link
+	 *
+	 * @param	string	the email address
+	 * @param	string	the link title
+	 * @param	mixed	any attributes
+	 * @return	string
+	 */
+	function mailto($email, $title = '', $attributes = '')
+	{
+		$title = (string) $title;
+
+		if ($title === '')
+		{
+			$title = $email;
+		}
+
+		return '<a href="mailto:'.$email.'"'._stringify_attributes($attributes).'>'.$title.'</a>';
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('safe_mailto'))
+{
+	/**
+	 * Encoded Mailto Link
+	 *
+	 * Create a spam-protected mailto link written in Javascript
+	 *
+	 * @param	string	the email address
+	 * @param	string	the link title
+	 * @param	mixed	any attributes
+	 * @return	string
+	 */
+	function safe_mailto($email, $title = '', $attributes = '')
+	{
+		$title = (string) $title;
+
+		if ($title === '')
+		{
+			$title = $email;
+		}
+
+		$x = str_split('<a href="mailto:', 1);
+
+		for ($i = 0, $l = strlen($email); $i < $l; $i++)
+		{
+			$x[] = '|'.ord($email[$i]);
+		}
+
+		$x[] = '"';
+
+		if ($attributes !== '')
+		{
+			if (is_array($attributes))
+			{
+				foreach ($attributes as $key => $val)
+				{
+					$x[] = ' '.$key.'="';
+					for ($i = 0, $l = strlen($val); $i < $l; $i++)
+					{
+						$x[] = '|'.ord($val[$i]);
+					}
+					$x[] = '"';
+				}
+			}
+			else
+			{
+				for ($i = 0, $l = strlen($attributes); $i < $l; $i++)
+				{
+					$x[] = $attributes[$i];
+				}
+			}
+		}
+
+		$x[] = '>';
+
+		$temp = array();
+		for ($i = 0, $l = strlen($title); $i < $l; $i++)
+		{
+			$ordinal = ord($title[$i]);
+
+			if ($ordinal < 128)
+			{
+				$x[] = '|'.$ordinal;
+			}
+			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);
+					$x[] = '|'.$number;
+					$count = 1;
+					$temp = array();
+				}
+			}
+		}
+
+		$x[] = '<'; $x[] = '/'; $x[] = 'a'; $x[] = '>';
+
+		$x = array_reverse($x);
+
+		$output = "<script type=\"text/javascript\">\n"
+			."\t//<![CDATA[\n"
+			."\tvar l=new Array();\n";
+
+		for ($i = 0, $c = count($x); $i < $c; $i++)
+		{
+			$output .= "\tl[".$i."] = '".$x[$i]."';\n";
+		}
+
+		$output .= "\n\tfor (var i = l.length-1; i >= 0; i=i-1) {\n"
+			."\t\tif (l[i].substring(0, 1) === '|') document.write(\"&#\"+unescape(l[i].substring(1))+\";\");\n"
+			."\t\telse document.write(unescape(l[i]));\n"
+			."\t}\n"
+			."\t//]]>\n"
+			.'</script>';
+
+		return $output;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('auto_link'))
+{
+	/**
+	 * Auto-linker
+	 *
+	 * Automatically links URL and Email addresses.
+	 * Note: There's a bit of extra code here to deal with
+	 * URLs or emails that end in a period. We'll strip these
+	 * off and add them after the link.
+	 *
+	 * @param	string	the string
+	 * @param	string	the type: email, url, or both
+	 * @param	bool	whether to create pop-up links
+	 * @return	string
+	 */
+	function auto_link($str, $type = 'both', $popup = FALSE)
+	{
+		// Find and replace any URLs.
+		if ($type !== 'email' && preg_match_all('#(\w*://|www\.)[a-z0-9]+(-+[a-z0-9]+)*(\.[a-z0-9]+(-+[a-z0-9]+)*)+(/([^\s()<>;]+\w)?/?)?#i', $str, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER))
+		{
+			// Set our target HTML if using popup links.
+			$target = ($popup) ? ' target="_blank" rel="noopener"' : '';
+
+			// We process the links in reverse order (last -> first) so that
+			// the returned string offsets from preg_match_all() are not
+			// moved as we add more HTML.
+			foreach (array_reverse($matches) as $match)
+			{
+				// $match[0] is the matched string/link
+				// $match[1] is either a protocol prefix or 'www.'
+				//
+				// With PREG_OFFSET_CAPTURE, both of the above is an array,
+				// where the actual value is held in [0] and its offset at the [1] index.
+				$a = '<a href="'.(strpos($match[1][0], '/') ? '' : 'http://').$match[0][0].'"'.$target.'>'.$match[0][0].'</a>';
+				$str = substr_replace($str, $a, $match[0][1], strlen($match[0][0]));
+			}
+		}
+
+		// Find and replace any emails.
+		if ($type !== 'url' && preg_match_all('#([\w\.\-\+]+@[a-z0-9\-]+\.[a-z0-9\-\.]+[^[:punct:]\s])#i', $str, $matches, PREG_OFFSET_CAPTURE))
+		{
+			foreach (array_reverse($matches[0]) as $match)
+			{
+				if (filter_var($match[0], FILTER_VALIDATE_EMAIL) !== FALSE)
+				{
+					$str = substr_replace($str, safe_mailto($match[0]), $match[1], strlen($match[0]));
+				}
+			}
+		}
+
+		return $str;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('prep_url'))
+{
+	/**
+	 * Prep URL
+	 *
+	 * Simply adds the http:// part if no scheme is included
+	 *
+	 * @param	string	the URL
+	 * @return	string
+	 */
+	function prep_url($str = '')
+	{
+		if ($str === 'http://' OR $str === '')
+		{
+			return '';
+		}
+
+		$url = parse_url($str);
+
+		if ( ! $url OR ! isset($url['scheme']))
+		{
+			return 'http://'.$str;
+		}
+
+		return $str;
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('url_title'))
+{
+	/**
+	 * Create URL Title
+	 *
+	 * Takes a "title" string as input and creates a
+	 * human-friendly URL string with a "separator" string
+	 * as the word separator.
+	 *
+	 * @todo	Remove old 'dash' and 'underscore' usage in 3.1+.
+	 * @param	string	$str		Input string
+	 * @param	string	$separator	Word separator
+	 *			(usually '-' or '_')
+	 * @param	bool	$lowercase	Whether to transform the output string to lowercase
+	 * @return	string
+	 */
+	function url_title($str, $separator = '-', $lowercase = FALSE)
+	{
+		if ($separator === 'dash')
+		{
+			$separator = '-';
+		}
+		elseif ($separator === 'underscore')
+		{
+			$separator = '_';
+		}
+
+		$q_separator = preg_quote($separator, '#');
+
+		$trans = array(
+			'&.+?;'			=> '',
+			'[^\w\d _-]'		=> '',
+			'\s+'			=> $separator,
+			'('.$q_separator.')+'	=> $separator
+		);
+
+		$str = strip_tags($str);
+		foreach ($trans as $key => $val)
+		{
+			$str = preg_replace('#'.$key.'#i'.(UTF8_ENABLED ? 'u' : ''), $val, $str);
+		}
+
+		if ($lowercase === TRUE)
+		{
+			$str = strtolower($str);
+		}
+
+		return trim(trim($str, $separator));
+	}
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('redirect'))
+{
+	/**
+	 * Header Redirect
+	 *
+	 * Header redirect in two flavors
+	 * For very fine grained control over headers, you could use the Output
+	 * Library's set_header() function.
+	 *
+	 * @param	string	$uri	URL
+	 * @param	string	$method	Redirect method
+	 *			'auto', 'location' or 'refresh'
+	 * @param	int	$code	HTTP Response status code
+	 * @return	void
+	 */
+	function redirect($uri = '', $method = 'auto', $code = NULL)
+	{
+		if ( ! preg_match('#^(\w+:)?//#i', $uri))
+		{
+			$uri = site_url($uri);
+		}
+
+		// IIS environment likely? Use 'refresh' for better compatibility
+		if ($method === 'auto' && isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS') !== FALSE)
+		{
+			$method = 'refresh';
+		}
+		elseif ($method !== 'refresh' && (empty($code) OR ! is_numeric($code)))
+		{
+			if (isset($_SERVER['SERVER_PROTOCOL'], $_SERVER['REQUEST_METHOD']) && $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.1')
+			{
+				$code = ($_SERVER['REQUEST_METHOD'] !== 'GET')
+					? 303	// reference: http://en.wikipedia.org/wiki/Post/Redirect/Get
+					: 307;
+			}
+			else
+			{
+				$code = 302;
+			}
+		}
+
+		switch ($method)
+		{
+			case 'refresh':
+				header('Refresh:0;url='.$uri);
+				break;
+			default:
+				header('Location: '.$uri, TRUE, $code);
+				break;
+		}
+		exit;
+	}
+}
diff --git a/system/helpers/xml_helper.php b/system/helpers/xml_helper.php
new file mode 100644
index 0000000..5e0861e
--- /dev/null
+++ b/system/helpers/xml_helper.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CodeIgniter XML Helpers
+ *
+ * @package		CodeIgniter
+ * @subpackage	Helpers
+ * @category	Helpers
+ * @author		EllisLab Dev Team
+ * @link		https://codeigniter.com/userguide3/helpers/xml_helper.html
+ */
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('xml_convert'))
+{
+	/**
+	 * Convert Reserved XML characters to Entities
+	 *
+	 * @param	string
+	 * @param	bool
+	 * @return	string
+	 */
+	function xml_convert($str, $protect_all = FALSE)
+	{
+		$temp = '__TEMP_AMPERSANDS__';
+
+		// Replace entities to temporary markers so that
+		// ampersands won't get messed up
+		$str = preg_replace('/&#(\d+);/', $temp.'\\1;', $str);
+
+		if ($protect_all === TRUE)
+		{
+			$str = preg_replace('/&(\w+);/', $temp.'\\1;', $str);
+		}
+
+		$str = str_replace(
+			array('&', '<', '>', '"', "'", '-'),
+			array('&amp;', '&lt;', '&gt;', '&quot;', '&apos;', '&#45;'),
+			$str
+		);
+
+		// Decode the temp markers back to entities
+		$str = preg_replace('/'.$temp.'(\d+);/', '&#\\1;', $str);
+
+		if ($protect_all === TRUE)
+		{
+			return preg_replace('/'.$temp.'(\w+);/', '&\\1;', $str);
+		}
+
+		return $str;
+	}
+}
diff --git a/system/index.html b/system/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/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/system/language/english/calendar_lang.php b/system/language/english/calendar_lang.php
new file mode 100644
index 0000000..35352d6
--- /dev/null
+++ b/system/language/english/calendar_lang.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+$lang['cal_su'] = 'Su';
+$lang['cal_mo'] = 'Mo';
+$lang['cal_tu'] = 'Tu';
+$lang['cal_we'] = 'We';
+$lang['cal_th'] = 'Th';
+$lang['cal_fr'] = 'Fr';
+$lang['cal_sa'] = 'Sa';
+$lang['cal_sun'] = 'Sun';
+$lang['cal_mon'] = 'Mon';
+$lang['cal_tue'] = 'Tue';
+$lang['cal_wed'] = 'Wed';
+$lang['cal_thu'] = 'Thu';
+$lang['cal_fri'] = 'Fri';
+$lang['cal_sat'] = 'Sat';
+$lang['cal_sunday'] = 'Sunday';
+$lang['cal_monday'] = 'Monday';
+$lang['cal_tuesday'] = 'Tuesday';
+$lang['cal_wednesday'] = 'Wednesday';
+$lang['cal_thursday'] = 'Thursday';
+$lang['cal_friday'] = 'Friday';
+$lang['cal_saturday'] = 'Saturday';
+$lang['cal_jan'] = 'Jan';
+$lang['cal_feb'] = 'Feb';
+$lang['cal_mar'] = 'Mar';
+$lang['cal_apr'] = 'Apr';
+$lang['cal_may'] = 'May';
+$lang['cal_jun'] = 'Jun';
+$lang['cal_jul'] = 'Jul';
+$lang['cal_aug'] = 'Aug';
+$lang['cal_sep'] = 'Sep';
+$lang['cal_oct'] = 'Oct';
+$lang['cal_nov'] = 'Nov';
+$lang['cal_dec'] = 'Dec';
+$lang['cal_january'] = 'January';
+$lang['cal_february'] = 'February';
+$lang['cal_march'] = 'March';
+$lang['cal_april'] = 'April';
+$lang['cal_mayl'] = 'May';
+$lang['cal_june'] = 'June';
+$lang['cal_july'] = 'July';
+$lang['cal_august'] = 'August';
+$lang['cal_september'] = 'September';
+$lang['cal_october'] = 'October';
+$lang['cal_november'] = 'November';
+$lang['cal_december'] = 'December';
diff --git a/system/language/english/date_lang.php b/system/language/english/date_lang.php
new file mode 100644
index 0000000..fd184df
--- /dev/null
+++ b/system/language/english/date_lang.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+$lang['date_year'] = 'Year';
+$lang['date_years'] = 'Years';
+$lang['date_month'] = 'Month';
+$lang['date_months'] = 'Months';
+$lang['date_week'] = 'Week';
+$lang['date_weeks'] = 'Weeks';
+$lang['date_day'] = 'Day';
+$lang['date_days'] = 'Days';
+$lang['date_hour'] = 'Hour';
+$lang['date_hours'] = 'Hours';
+$lang['date_minute'] = 'Minute';
+$lang['date_minutes'] = 'Minutes';
+$lang['date_second'] = 'Second';
+$lang['date_seconds'] = 'Seconds';
+
+$lang['UM12']	= '(UTC -12:00) Baker/Howland Island';
+$lang['UM11']	= '(UTC -11:00) Niue';
+$lang['UM10']	= '(UTC -10:00) Hawaii-Aleutian Standard Time, Cook Islands, Tahiti';
+$lang['UM95']	= '(UTC -9:30) Marquesas Islands';
+$lang['UM9']	= '(UTC -9:00) Alaska Standard Time, Gambier Islands';
+$lang['UM8']	= '(UTC -8:00) Pacific Standard Time, Clipperton Island';
+$lang['UM7']	= '(UTC -7:00) Mountain Standard Time';
+$lang['UM6']	= '(UTC -6:00) Central Standard Time';
+$lang['UM5']	= '(UTC -5:00) Eastern Standard Time, Western Caribbean Standard Time';
+$lang['UM45']	= '(UTC -4:30) Venezuelan Standard Time';
+$lang['UM4']	= '(UTC -4:00) Atlantic Standard Time, Eastern Caribbean Standard Time';
+$lang['UM35']	= '(UTC -3:30) Newfoundland Standard Time';
+$lang['UM3']	= '(UTC -3:00) Argentina, Brazil, French Guiana, Uruguay';
+$lang['UM2']	= '(UTC -2:00) South Georgia/South Sandwich Islands';
+$lang['UM1']	= '(UTC -1:00) Azores, Cape Verde Islands';
+$lang['UTC']	= '(UTC) Greenwich Mean Time, Western European Time';
+$lang['UP1']	= '(UTC +1:00) Central European Time, West Africa Time';
+$lang['UP2']	= '(UTC +2:00) Central Africa Time, Eastern European Time, Kaliningrad Time';
+$lang['UP3']	= '(UTC +3:00) Moscow Time, East Africa Time, Arabia Standard Time';
+$lang['UP35']	= '(UTC +3:30) Iran Standard Time';
+$lang['UP4']	= '(UTC +4:00) Azerbaijan Standard Time, Samara Time';
+$lang['UP45']	= '(UTC +4:30) Afghanistan';
+$lang['UP5']	= '(UTC +5:00) Pakistan Standard Time, Yekaterinburg Time';
+$lang['UP55']	= '(UTC +5:30) Indian Standard Time, Sri Lanka Time';
+$lang['UP575']	= '(UTC +5:45) Nepal Time';
+$lang['UP6']	= '(UTC +6:00) Bangladesh Standard Time, Bhutan Time, Omsk Time';
+$lang['UP65']	= '(UTC +6:30) Cocos Islands, Myanmar';
+$lang['UP7']	= '(UTC +7:00) Krasnoyarsk Time, Cambodia, Laos, Thailand, Vietnam';
+$lang['UP8']	= '(UTC +8:00) Australian Western Standard Time, Beijing Time, Irkutsk Time';
+$lang['UP875']	= '(UTC +8:45) Australian Central Western Standard Time';
+$lang['UP9']	= '(UTC +9:00) Japan Standard Time, Korea Standard Time, Yakutsk Time';
+$lang['UP95']	= '(UTC +9:30) Australian Central Standard Time';
+$lang['UP10']	= '(UTC +10:00) Australian Eastern Standard Time, Vladivostok Time';
+$lang['UP105']	= '(UTC +10:30) Lord Howe Island';
+$lang['UP11']	= '(UTC +11:00) Srednekolymsk Time, Solomon Islands, Vanuatu';
+$lang['UP115']	= '(UTC +11:30) Norfolk Island';
+$lang['UP12']	= '(UTC +12:00) Fiji, Gilbert Islands, Kamchatka Time, New Zealand Standard Time';
+$lang['UP1275']	= '(UTC +12:45) Chatham Islands Standard Time';
+$lang['UP13']	= '(UTC +13:00) Samoa Time Zone, Phoenix Islands Time, Tonga';
+$lang['UP14']	= '(UTC +14:00) Line Islands';
diff --git a/system/language/english/db_lang.php b/system/language/english/db_lang.php
new file mode 100644
index 0000000..1bf424e
--- /dev/null
+++ b/system/language/english/db_lang.php
@@ -0,0 +1,64 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+$lang['db_invalid_connection_str'] = 'Unable to determine the database settings based on the connection string you submitted.';
+$lang['db_unable_to_connect'] = 'Unable to connect to your database server using the provided settings.';
+$lang['db_unable_to_select'] = 'Unable to select the specified database: %s';
+$lang['db_unable_to_create'] = 'Unable to create the specified database: %s';
+$lang['db_invalid_query'] = 'The query you submitted is not valid.';
+$lang['db_must_set_table'] = 'You must set the database table to be used with your query.';
+$lang['db_must_use_set'] = 'You must use the "set" method to update an entry.';
+$lang['db_must_use_index'] = 'You must specify an index to match on for batch updates.';
+$lang['db_batch_missing_index'] = 'One or more rows submitted for batch updating is missing the specified index.';
+$lang['db_must_use_where'] = 'Updates are not allowed unless they contain a "where" clause.';
+$lang['db_del_must_use_where'] = 'Deletes are not allowed unless they contain a "where" or "like" clause.';
+$lang['db_field_param_missing'] = 'To fetch fields requires the name of the table as a parameter.';
+$lang['db_unsupported_function'] = 'This feature is not available for the database you are using.';
+$lang['db_transaction_failure'] = 'Transaction failure: Rollback performed.';
+$lang['db_unable_to_drop'] = 'Unable to drop the specified database.';
+$lang['db_unsupported_feature'] = 'Unsupported feature of the database platform you are using.';
+$lang['db_unsupported_compression'] = 'The file compression format you chose is not supported by your server.';
+$lang['db_filepath_error'] = 'Unable to write data to the file path you have submitted.';
+$lang['db_invalid_cache_path'] = 'The cache path you submitted is not valid or writable.';
+$lang['db_table_name_required'] = 'A table name is required for that operation.';
+$lang['db_column_name_required'] = 'A column name is required for that operation.';
+$lang['db_column_definition_required'] = 'A column definition is required for that operation.';
+$lang['db_unable_to_set_charset'] = 'Unable to set client connection character set: %s';
+$lang['db_error_heading'] = 'A Database Error Occurred';
diff --git a/system/language/english/email_lang.php b/system/language/english/email_lang.php
new file mode 100644
index 0000000..7ed083c
--- /dev/null
+++ b/system/language/english/email_lang.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) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+$lang['email_must_be_array'] = 'The email validation method must be passed an array.';
+$lang['email_invalid_address'] = 'Invalid email address: %s';
+$lang['email_attachment_missing'] = 'Unable to locate the following email attachment: %s';
+$lang['email_attachment_unreadable'] = 'Unable to open this attachment: %s';
+$lang['email_no_from'] = 'Cannot send mail with no "From" header.';
+$lang['email_no_recipients'] = 'You must include recipients: To, Cc, or Bcc';
+$lang['email_send_failure_phpmail'] = 'Unable to send email using PHP mail(). Your server might not be configured to send mail using this method.';
+$lang['email_send_failure_sendmail'] = 'Unable to send email using PHP Sendmail. Your server might not be configured to send mail using this method.';
+$lang['email_send_failure_smtp'] = 'Unable to send email using PHP SMTP. Your server might not be configured to send mail using this method.';
+$lang['email_sent'] = 'Your message has been successfully sent using the following protocol: %s';
+$lang['email_no_socket'] = 'Unable to open a socket to Sendmail. Please check settings.';
+$lang['email_no_hostname'] = 'You did not specify a SMTP hostname.';
+$lang['email_smtp_error'] = 'The following SMTP error was encountered: %s';
+$lang['email_no_smtp_unpw'] = 'Error: You must assign a SMTP username and password.';
+$lang['email_failed_smtp_login'] = 'Failed to send AUTH LOGIN command. Error: %s';
+$lang['email_smtp_auth_un'] = 'Failed to authenticate username. Error: %s';
+$lang['email_smtp_auth_pw'] = 'Failed to authenticate password. Error: %s';
+$lang['email_smtp_data_failure'] = 'Unable to send data: %s';
+$lang['email_exit_status'] = 'Exit status code: %s';
diff --git a/system/language/english/form_validation_lang.php b/system/language/english/form_validation_lang.php
new file mode 100644
index 0000000..a2e300e
--- /dev/null
+++ b/system/language/english/form_validation_lang.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+$lang['form_validation_required']		= 'The {field} field is required.';
+$lang['form_validation_isset']			= 'The {field} field must have a value.';
+$lang['form_validation_valid_email']		= 'The {field} field must contain a valid email address.';
+$lang['form_validation_valid_emails']		= 'The {field} field must contain all valid email addresses.';
+$lang['form_validation_valid_url']		= 'The {field} field must contain a valid URL.';
+$lang['form_validation_valid_ip']		= 'The {field} field must contain a valid IP.';
+$lang['form_validation_valid_base64']		= 'The {field} field must contain a valid Base64 string.';
+$lang['form_validation_min_length']		= 'The {field} field must be at least {param} characters in length.';
+$lang['form_validation_max_length']		= 'The {field} field cannot exceed {param} characters in length.';
+$lang['form_validation_exact_length']		= 'The {field} field must be exactly {param} characters in length.';
+$lang['form_validation_alpha']			= 'The {field} field may only contain alphabetical characters.';
+$lang['form_validation_alpha_numeric']		= 'The {field} field may only contain alpha-numeric characters.';
+$lang['form_validation_alpha_numeric_spaces']	= 'The {field} field may only contain alpha-numeric characters and spaces.';
+$lang['form_validation_alpha_dash']		= 'The {field} field may only contain alpha-numeric characters, underscores, and dashes.';
+$lang['form_validation_numeric']		= 'The {field} field must contain only numbers.';
+$lang['form_validation_is_numeric']		= 'The {field} field must contain only numeric characters.';
+$lang['form_validation_integer']		= 'The {field} field must contain an integer.';
+$lang['form_validation_regex_match']		= 'The {field} field is not in the correct format.';
+$lang['form_validation_matches']		= 'The {field} field does not match the {param} field.';
+$lang['form_validation_differs']		= 'The {field} field must differ from the {param} field.';
+$lang['form_validation_is_unique'] 		= 'The {field} field must contain a unique value.';
+$lang['form_validation_is_natural']		= 'The {field} field must only contain digits.';
+$lang['form_validation_is_natural_no_zero']	= 'The {field} field must only contain digits and must be greater than zero.';
+$lang['form_validation_decimal']		= 'The {field} field must contain a decimal number.';
+$lang['form_validation_less_than']		= 'The {field} field must contain a number less than {param}.';
+$lang['form_validation_less_than_equal_to']	= 'The {field} field must contain a number less than or equal to {param}.';
+$lang['form_validation_greater_than']		= 'The {field} field must contain a number greater than {param}.';
+$lang['form_validation_greater_than_equal_to']	= 'The {field} field must contain a number greater than or equal to {param}.';
+$lang['form_validation_error_message_not_set']	= 'Unable to access an error message corresponding to your field name {field}.';
+$lang['form_validation_in_list']		= 'The {field} field must be one of: {param}.';
diff --git a/system/language/english/ftp_lang.php b/system/language/english/ftp_lang.php
new file mode 100644
index 0000000..7067b4b
--- /dev/null
+++ b/system/language/english/ftp_lang.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+$lang['ftp_no_connection']		= 'Unable to locate a valid connection ID. Please make sure you are connected before performing any file routines.';
+$lang['ftp_unable_to_connect']		= 'Unable to connect to your FTP server using the supplied hostname.';
+$lang['ftp_unable_to_login']		= 'Unable to login to your FTP server. Please check your username and password.';
+$lang['ftp_unable_to_mkdir']		= 'Unable to create the directory you have specified.';
+$lang['ftp_unable_to_changedir']	= 'Unable to change directories.';
+$lang['ftp_unable_to_chmod']		= 'Unable to set file permissions. Please check your path.';
+$lang['ftp_unable_to_upload']		= 'Unable to upload the specified file. Please check your path.';
+$lang['ftp_unable_to_download']		= 'Unable to download the specified file. Please check your path.';
+$lang['ftp_no_source_file']		= 'Unable to locate the source file. Please check your path.';
+$lang['ftp_unable_to_rename']		= 'Unable to rename the file.';
+$lang['ftp_unable_to_delete']		= 'Unable to delete the file.';
+$lang['ftp_unable_to_move']		= 'Unable to move the file. Please make sure the destination directory exists.';
diff --git a/system/language/english/imglib_lang.php b/system/language/english/imglib_lang.php
new file mode 100644
index 0000000..01ac9d3
--- /dev/null
+++ b/system/language/english/imglib_lang.php
@@ -0,0 +1,58 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+$lang['imglib_source_image_required'] = 'You must specify a source image in your preferences.';
+$lang['imglib_gd_required'] = 'The GD image library is required for this feature.';
+$lang['imglib_gd_required_for_props'] = 'Your server must support the GD image library in order to determine the image properties.';
+$lang['imglib_unsupported_imagecreate'] = 'Your server does not support the GD function required to process this type of image.';
+$lang['imglib_gif_not_supported'] = 'GIF images are often not supported due to licensing restrictions. You may have to use JPG or PNG images instead.';
+$lang['imglib_jpg_not_supported'] = 'JPG images are not supported.';
+$lang['imglib_png_not_supported'] = 'PNG images are not supported.';
+$lang['imglib_jpg_or_png_required'] = 'The image resize protocol specified in your preferences only works with JPEG or PNG image types.';
+$lang['imglib_copy_error'] = 'An error was encountered while attempting to replace the file. Please make sure your file directory is writable.';
+$lang['imglib_rotate_unsupported'] = 'Image rotation does not appear to be supported by your server.';
+$lang['imglib_libpath_invalid'] = 'The path to your image library is not correct. Please set the correct path in your image preferences.';
+$lang['imglib_image_process_failed'] = 'Image processing failed. Please verify that your server supports the chosen protocol and that the path to your image library is correct.';
+$lang['imglib_rotation_angle_required'] = 'An angle of rotation is required to rotate the image.';
+$lang['imglib_invalid_path'] = 'The path to the image is not correct.';
+$lang['imglib_invalid_image'] = 'The provided image is not valid.';
+$lang['imglib_copy_failed'] = 'The image copy routine failed.';
+$lang['imglib_missing_font'] = 'Unable to find a font to use.';
+$lang['imglib_save_failed'] = 'Unable to save the image. Please make sure the image and file directory are writable.';
diff --git a/system/language/english/index.html b/system/language/english/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/language/english/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/system/language/english/migration_lang.php b/system/language/english/migration_lang.php
new file mode 100644
index 0000000..a370362
--- /dev/null
+++ b/system/language/english/migration_lang.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+$lang['migration_none_found'] = 'No migrations were found.';
+$lang['migration_not_found'] = 'No migration could be found with the version number: %s.';
+$lang['migration_sequence_gap'] = 'There is a gap in the migration sequence near version number: %s.';
+$lang['migration_multiple_version'] = 'There are multiple migrations with the same version number: %s.';
+$lang['migration_class_doesnt_exist'] = 'The migration class "%s" could not be found.';
+$lang['migration_missing_up_method'] = 'The migration class "%s" is missing an "up" method.';
+$lang['migration_missing_down_method'] = 'The migration class "%s" is missing a "down" method.';
+$lang['migration_invalid_filename'] = 'Migration "%s" has an invalid filename.';
diff --git a/system/language/english/number_lang.php b/system/language/english/number_lang.php
new file mode 100644
index 0000000..38e3781
--- /dev/null
+++ b/system/language/english/number_lang.php
@@ -0,0 +1,45 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+$lang['terabyte_abbr'] = 'TB';
+$lang['gigabyte_abbr'] = 'GB';
+$lang['megabyte_abbr'] = 'MB';
+$lang['kilobyte_abbr'] = 'KB';
+$lang['bytes'] = 'Bytes';
diff --git a/system/language/english/pagination_lang.php b/system/language/english/pagination_lang.php
new file mode 100644
index 0000000..808a61e
--- /dev/null
+++ b/system/language/english/pagination_lang.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+$lang['pagination_first_link'] = '&lsaquo; First';
+$lang['pagination_next_link'] = '&gt;';
+$lang['pagination_prev_link'] = '&lt;';
+$lang['pagination_last_link'] = 'Last &rsaquo;';
diff --git a/system/language/english/profiler_lang.php b/system/language/english/profiler_lang.php
new file mode 100644
index 0000000..71a2afc
--- /dev/null
+++ b/system/language/english/profiler_lang.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+$lang['profiler_database'] = 'DATABASE';
+$lang['profiler_controller_info'] = 'CLASS/METHOD';
+$lang['profiler_benchmarks'] = 'BENCHMARKS';
+$lang['profiler_queries'] = 'QUERIES';
+$lang['profiler_get_data'] = 'GET DATA';
+$lang['profiler_post_data'] = 'POST DATA';
+$lang['profiler_uri_string'] = 'URI STRING';
+$lang['profiler_memory_usage'] = 'MEMORY USAGE';
+$lang['profiler_config'] = 'CONFIG VARIABLES';
+$lang['profiler_session_data'] = 'SESSION DATA';
+$lang['profiler_headers'] = 'HTTP HEADERS';
+$lang['profiler_no_db'] = 'Database driver is not currently loaded';
+$lang['profiler_no_queries'] = 'No queries were run';
+$lang['profiler_no_post'] = 'No POST data exists';
+$lang['profiler_no_get'] = 'No GET data exists';
+$lang['profiler_no_uri'] = 'No URI data exists';
+$lang['profiler_no_memory'] = 'Memory Usage Unavailable';
+$lang['profiler_no_profiles'] = 'No Profile data - all Profiler sections have been disabled.';
+$lang['profiler_section_hide'] = 'Hide';
+$lang['profiler_section_show'] = 'Show';
+$lang['profiler_seconds'] = 'seconds';
diff --git a/system/language/english/unit_test_lang.php b/system/language/english/unit_test_lang.php
new file mode 100644
index 0000000..02366f0
--- /dev/null
+++ b/system/language/english/unit_test_lang.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) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+$lang['ut_test_name'] = 'Test Name';
+$lang['ut_test_datatype'] = 'Test Datatype';
+$lang['ut_res_datatype'] = 'Expected Datatype';
+$lang['ut_result'] = 'Result';
+$lang['ut_undefined'] = 'Undefined Test Name';
+$lang['ut_file'] = 'File Name';
+$lang['ut_line'] = 'Line Number';
+$lang['ut_passed'] = 'Passed';
+$lang['ut_failed'] = 'Failed';
+$lang['ut_boolean'] = 'Boolean';
+$lang['ut_integer'] = 'Integer';
+$lang['ut_float'] = 'Float';
+$lang['ut_double'] = 'Float'; // can be the same as float
+$lang['ut_string'] = 'String';
+$lang['ut_array'] = 'Array';
+$lang['ut_object'] = 'Object';
+$lang['ut_resource'] = 'Resource';
+$lang['ut_null'] = 'Null';
+$lang['ut_notes'] = 'Notes';
diff --git a/system/language/english/upload_lang.php b/system/language/english/upload_lang.php
new file mode 100644
index 0000000..bd1e201
--- /dev/null
+++ b/system/language/english/upload_lang.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 1.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+$lang['upload_userfile_not_set'] = 'Unable to find a post variable called userfile.';
+$lang['upload_file_exceeds_limit'] = 'The uploaded file exceeds the maximum allowed size in your PHP configuration file.';
+$lang['upload_file_exceeds_form_limit'] = 'The uploaded file exceeds the maximum size allowed by the submission form.';
+$lang['upload_file_partial'] = 'The file was only partially uploaded.';
+$lang['upload_no_temp_directory'] = 'The temporary folder is missing.';
+$lang['upload_unable_to_write_file'] = 'The file could not be written to disk.';
+$lang['upload_stopped_by_extension'] = 'The file upload was stopped by extension.';
+$lang['upload_no_file_selected'] = 'You did not select a file to upload.';
+$lang['upload_invalid_filetype'] = 'The filetype you are attempting to upload is not allowed.';
+$lang['upload_invalid_filesize'] = 'The file you are attempting to upload is larger than the permitted size.';
+$lang['upload_invalid_dimensions'] = 'The image you are attempting to upload doesn\'t fit into the allowed dimensions.';
+$lang['upload_destination_error'] = 'A problem was encountered while attempting to move the uploaded file to the final destination.';
+$lang['upload_no_filepath'] = 'The upload path does not appear to be valid.';
+$lang['upload_no_file_types'] = 'You have not specified any allowed file types.';
+$lang['upload_bad_filename'] = 'The file name you submitted already exists on the server.';
+$lang['upload_not_writable'] = 'The upload destination folder does not appear to be writable.';
diff --git a/system/language/index.html b/system/language/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/language/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/system/libraries/Cache/Cache.php b/system/libraries/Cache/Cache.php
new file mode 100644
index 0000000..d0c4c88
--- /dev/null
+++ b/system/libraries/Cache/Cache.php
@@ -0,0 +1,256 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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/system/libraries/Cache/drivers/Cache_apc.php b/system/libraries/Cache/drivers/Cache_apc.php
new file mode 100644
index 0000000..2299204
--- /dev/null
+++ b/system/libraries/Cache/drivers/Cache_apc.php
@@ -0,0 +1,218 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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 {
+
+	/**
+	 * Class constructor
+	 *
+	 * Only present so that an error message is logged
+	 * if APC is not available.
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		if ( ! $this->is_supported())
+		{
+			log_message('error', 'Cache: Failed to initialize APC; extension not loaded/enabled?');
+		}
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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);
+
+		return ($success === TRUE) ? $data : FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Cache Save
+	 *
+	 * @param	string	$id	Cache ID
+	 * @param	mixed	$data	Data to store
+	 * @param	int	$ttl	Length of time (in seconds) to cache the data
+	 * @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 apc_store($id, $data, (int) $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)
+	{
+		$cache_info = apc_cache_info('user', FALSE);
+		if (empty($cache_info) OR empty($cache_info['cache_list']))
+		{
+			return FALSE;
+		}
+
+		foreach ($cache_info['cache_list'] as &$entry)
+		{
+			if ($entry['info'] !== $id)
+			{
+				continue;
+			}
+
+			$success  = FALSE;
+			$metadata = array(
+				'expire' => ($entry['ttl'] ? $entry['mtime'] + $entry['ttl'] : 0),
+				'mtime'  => $entry['ttl'],
+				'data'   => apc_fetch($id, $success)
+			);
+
+			return ($success === TRUE) ? $metadata : FALSE;
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * is_supported()
+	 *
+	 * Check to see if APC is available on this system, bail if it isn't.
+	 *
+	 * @return	bool
+	 */
+	public function is_supported()
+	{
+		return (extension_loaded('apc') && ini_get('apc.enabled'));
+	}
+}
diff --git a/system/libraries/Cache/drivers/Cache_dummy.php b/system/libraries/Cache/drivers/Cache_dummy.php
new file mode 100644
index 0000000..f3ca220
--- /dev/null
+++ b/system/libraries/Cache/drivers/Cache_dummy.php
@@ -0,0 +1,173 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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/system/libraries/Cache/drivers/Cache_file.php b/system/libraries/Cache/drivers/Cache_file.php
new file mode 100644
index 0000000..3a4be98
--- /dev/null
+++ b/system/libraries/Cache/drivers/Cache_file.php
@@ -0,0 +1,287 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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 is_file($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 ( ! is_file($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'], $data['time']))
+			{
+				return FALSE;
+			}
+
+			return array(
+				'expire' => $data['time'] + $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'])
+		{
+			file_exists($this->_cache_path.$id) && unlink($this->_cache_path.$id);
+			return FALSE;
+		}
+
+		return $data;
+	}
+
+}
diff --git a/system/libraries/Cache/drivers/Cache_memcached.php b/system/libraries/Cache/drivers/Cache_memcached.php
new file mode 100644
index 0000000..89002de
--- /dev/null
+++ b/system/libraries/Cache/drivers/Cache_memcached.php
@@ -0,0 +1,314 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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 $_config = 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->_config['default'];
+
+		if ($CI->config->load('memcached', TRUE, TRUE))
+		{
+			$this->_config = $CI->config->config['memcached'];
+		}
+
+		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?');
+			return;
+		}
+
+		foreach ($this->_config 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 ($this->_memcached instanceof Memcache)
+			{
+				// Third parameter is persistence and defaults to TRUE.
+				$this->_memcached->addServer(
+					$cache_server['hostname'],
+					$cache_server['port'],
+					TRUE,
+					$cache_server['weight']
+				);
+			}
+			elseif ($this->_memcached instanceof Memcached)
+			{
+				$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 ($this->_memcached instanceof Memcached)
+		{
+			return $this->_memcached->set($id, $data, $ttl);
+		}
+		elseif ($this->_memcached instanceof Memcache)
+		{
+			return $this->_memcached->set($id, $data, 0, $ttl);
+		}
+
+		return FALSE;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Delete from Cache
+	 *
+	 * @param	mixed	$id	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)
+	{
+		if (($result = $this->_memcached->increment($id, $offset)) === FALSE)
+		{
+			return $this->_memcached->add($id, $offset) ? $offset : FALSE;
+		}
+
+		return $result;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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)
+	{
+		if (($result = $this->_memcached->decrement($id, $offset)) === FALSE)
+		{
+			return $this->_memcached->add($id, 0) ? 0 : FALSE;
+		}
+
+		return $result;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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	$id	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'));
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Class destructor
+	 *
+	 * Closes the connection to Memcache(d) if present.
+	 *
+	 * @return	void
+	 */
+	public function __destruct()
+	{
+		if ($this->_memcached instanceof Memcache)
+		{
+			$this->_memcached->close();
+		}
+		elseif ($this->_memcached instanceof Memcached && method_exists($this->_memcached, 'quit'))
+		{
+			$this->_memcached->quit();
+		}
+	}
+}
diff --git a/system/libraries/Cache/drivers/Cache_redis.php b/system/libraries/Cache/drivers/Cache_redis.php
new file mode 100644
index 0000000..e8dd9b3
--- /dev/null
+++ b/system/libraries/Cache/drivers/Cache_redis.php
@@ -0,0 +1,348 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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();
+
+	/**
+	 * del()/delete() method name depending on phpRedis version
+	 *
+	 * @var	string
+	 */
+	protected static $_delete_name;
+
+	/**
+	 * sRem()/sRemove() method name depending on phpRedis version
+	 *
+	 * @var	string
+	 */
+	protected static $_sRemove_name;
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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()
+	{
+		if ( ! $this->is_supported())
+		{
+			log_message('error', 'Cache: Failed to create Redis object; extension not loaded?');
+			return;
+		}
+
+		if ( ! isset(static::$_delete_name, static::$_sRemove_name))
+		{
+			if (version_compare(phpversion('redis'), '5', '>='))
+			{
+				static::$_delete_name  = 'del';
+				static::$_sRemove_name = 'sRem';
+			}
+			else
+			{
+				static::$_delete_name  = 'delete';
+				static::$_sRemove_name = 'sRemove';
+			}
+		}
+
+		$CI =& get_instance();
+
+		if ($CI->config->load('redis', TRUE, TRUE))
+		{
+			$config = array_merge(self::$_default_config, $CI->config->item('redis'));
+		}
+		else
+		{
+			$config = self::$_default_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().')');
+		}
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get cache
+	 *
+	 * @param	string	$key	Cache ID
+	 * @return	mixed
+	 */
+	public function get($key)
+	{
+		$value = $this->_redis->get($key);
+
+		if ($value !== FALSE && $this->_redis->sIsMember('_ci_redis_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);
+		}
+		else
+		{
+			$this->_redis->{static::$_sRemove_name}('_ci_redis_serialized', $id);
+		}
+
+		return $this->_redis->set($id, $data, $ttl);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Delete from cache
+	 *
+	 * @param	string	$key	Cache key
+	 * @return	bool
+	 */
+	public function delete($key)
+	{
+		if ($this->_redis->{static::$_delete_name}($key) !== 1)
+		{
+			return FALSE;
+		}
+
+		$this->_redis->{static::$_sRemove_name}('_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->incrBy($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->decrBy($id, $offset);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Clean cache
+	 *
+	 * @return	bool
+	 * @see		Redis::flushDB()
+	 */
+	public function clean()
+	{
+		return $this->_redis->flushDB();
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get cache driver info
+	 *
+	 * @param	string	$type	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	$key	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/system/libraries/Cache/drivers/Cache_wincache.php b/system/libraries/Cache/drivers/Cache_wincache.php
new file mode 100644
index 0000000..bd18148
--- /dev/null
+++ b/system/libraries/Cache/drivers/Cache_wincache.php
@@ -0,0 +1,218 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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:
+ * https://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 {
+
+	/**
+	 * Class constructor
+	 *
+	 * Only present so that an error message is logged
+	 * if APC is not available.
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		if ( ! $this->is_supported())
+		{
+			log_message('error', 'Cache: Failed to initialize Wincache; extension not loaded/enabled?');
+		}
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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()
+	{
+		return (extension_loaded('wincache') && ini_get('wincache.ucenabled'));
+	}
+}
diff --git a/system/libraries/Cache/drivers/index.html b/system/libraries/Cache/drivers/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/libraries/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/system/libraries/Cache/index.html b/system/libraries/Cache/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/libraries/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/system/libraries/Calendar.php b/system/libraries/Calendar.php
new file mode 100644
index 0000000..8eefc82
--- /dev/null
+++ b/system/libraries/Calendar.php
@@ -0,0 +1,547 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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/system/libraries/Cart.php b/system/libraries/Cart.php
new file mode 100644
index 0000000..f8244b1
--- /dev/null
+++ b/system/libraries/Cart.php
@@ -0,0 +1,568 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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/system/libraries/Driver.php b/system/libraries/Driver.php
new file mode 100644
index 0000000..84f0b6c
--- /dev/null
+++ b/system/libraries/Driver.php
@@ -0,0 +1,343 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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/system/libraries/Email.php b/system/libraries/Email.php
new file mode 100644
index 0000000..82fae12
--- /dev/null
+++ b/system/libraries/Email.php
@@ -0,0 +1,2491 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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';
+
+	/**
+	 * 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		= '';
+
+	/**
+	 * 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)'
+	);
+
+	/**
+	 * mbstring.func_overload flag
+	 *
+	 * @var	bool
+	 */
+	protected static $func_overload;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * 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');
+		$this->initialize($config);
+		$this->_safe_mode = ( ! is_php('5.4') && ini_get('safe_mode'));
+
+		isset(self::$func_overload) OR self::$func_overload = ( ! is_php('8.0') && extension_loaded('mbstring') && @ini_get('mbstring.func_overload'));
+
+		log_message('info', 'Email Class Initialized');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Initialize preferences
+	 *
+	 * @param	array	$config
+	 * @return	CI_Email
+	 */
+	public function initialize(array $config = array())
+	{
+		$this->clear();
+
+		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->charset = strtoupper($this->charset);
+		$this->_smtp_auth = isset($this->smtp_user[0], $this->smtp_pass[0]);
+
+		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('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 !== '')
+		{
+			// 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('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)),
+			'multipart'	=> 'mixed'
+		);
+
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Set and return attachment Content-ID
+	 *
+	 * Useful for attached inline pictures
+	 *
+	 * @param	string	$filename
+	 * @return	string
+	 */
+	public function attachment_cid($filename)
+	{
+		for ($i = 0, $c = count($this->_attachments); $i < $c; $i++)
+		{
+			if ($this->_attachments[$i]['name'][0] === $filename)
+			{
+				$this->_attachments[$i]['multipart'] = 'related';
+				$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;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * 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
+	 *
+	 * @return	mixed
+	 */
+	protected function _get_protocol()
+	{
+		$this->protocol = strtolower($this->protocol);
+		in_array($this->protocol, $this->_protocols, TRUE) OR $this->protocol = 'mail';
+		return $this->protocol;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get Mail Encoding
+	 *
+	 * @return	string
+	 */
+	protected function _get_encoding()
+	{
+		in_array($this->_encoding, $this->_bit_depths) OR $this->_encoding = '8bit';
+
+		foreach ($this->_base_charsets as $charset)
+		{
+			if (strpos($this->charset, $charset) === 0)
+			{
+				$this->_encoding = '7bit';
+			}
+		}
+
+		return $this->_encoding;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get content type (text/html/attachment)
+	 *
+	 * @return	string
+	 */
+	protected function _get_content_type()
+	{
+		if ($this->mailtype === 'html')
+		{
+			return empty($this->_attachments) ? 'html' : 'html-attach';
+		}
+		elseif	($this->mailtype === 'text' && ! empty($this->_attachments))
+		{
+			return 'plain-attach';
+		}
+
+		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') && strpos($email, '@'))
+		{
+			list($account, $domain) = explode('@', $email, 2);
+			$domain = defined('INTL_IDNA_VARIANT_UTS46')
+				? idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46)
+				: idn_to_ascii($domain);
+
+			if ($domain !== FALSE)
+			{
+				$email = $account.'@'.$domain;
+			}
+		}
+
+		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 (self::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 .= self::substr($line, 0, $charlim - 1);
+				$line = self::substr($line, $charlim - 1);
+			}
+			while (self::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	void
+	 */
+	protected function _build_headers()
+	{
+		$this->set_header('User-Agent', $this->useragent);
+		$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->_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
+				{
+					$boundary = uniqid('B_ALT_');
+					$hdr .= 'Content-Type: multipart/alternative; boundary="'.$boundary.'"';
+
+					$body .= $this->_get_mime_message().$this->newline.$this->newline
+						.'--'.$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
+						.'--'.$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 .= '--'.$boundary.'--';
+				}
+
+				return;
+
+			case 'plain-attach':
+
+				$boundary = uniqid('B_ATC_');
+				$hdr .= 'Content-Type: multipart/mixed; boundary="'.$boundary.'"';
+
+				if ($this->_get_protocol() === 'mail')
+				{
+					$this->_header_str .= $hdr;
+				}
+
+				$body .= $this->_get_mime_message().$this->newline
+					.$this->newline
+					.'--'.$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;
+
+				$this->_append_attachments($body, $boundary);
+
+				break;
+			case 'html-attach':
+
+				$alt_boundary = uniqid('B_ALT_');
+				$last_boundary = NULL;
+
+				if ($this->_attachments_have_multipart('mixed'))
+				{
+					$atc_boundary = uniqid('B_ATC_');
+					$hdr .= 'Content-Type: multipart/mixed; boundary="'.$atc_boundary.'"';
+					$last_boundary = $atc_boundary;
+				}
+
+				if ($this->_attachments_have_multipart('related'))
+				{
+					$rel_boundary = uniqid('B_REL_');
+					$rel_boundary_header = 'Content-Type: multipart/related; boundary="'.$rel_boundary.'"';
+
+					if (isset($last_boundary))
+					{
+						$body .= '--'.$last_boundary.$this->newline.$rel_boundary_header;
+					}
+					else
+					{
+						$hdr .= $rel_boundary_header;
+					}
+
+					$last_boundary = $rel_boundary;
+				}
+
+				if ($this->_get_protocol() === 'mail')
+				{
+					$this->_header_str .= $hdr;
+				}
+
+				self::strlen($body) && $body .= $this->newline.$this->newline;
+				$body .= $this->_get_mime_message().$this->newline.$this->newline
+					.'--'.$last_boundary.$this->newline
+
+					.'Content-Type: multipart/alternative; boundary="'.$alt_boundary.'"'.$this->newline.$this->newline
+					.'--'.$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
+					.'--'.$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
+					.'--'.$alt_boundary.'--'.$this->newline.$this->newline;
+
+				if ( ! empty($rel_boundary))
+				{
+					$body .= $this->newline.$this->newline;
+					$this->_append_attachments($body, $rel_boundary, 'related');
+				}
+
+				// multipart/mixed attachments
+				if ( ! empty($atc_boundary))
+				{
+					$body .= $this->newline.$this->newline;
+					$this->_append_attachments($body, $atc_boundary, 'mixed');
+				}
+
+				break;
+		}
+
+		$this->_finalbody = ($this->_get_protocol() === 'mail')
+			? $body
+			: $hdr.$this->newline.$this->newline.$body;
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	protected function _attachments_have_multipart($type)
+	{
+		foreach ($this->_attachments as &$attachment)
+		{
+			if ($attachment['multipart'] === $type)
+			{
+				return TRUE;
+			}
+		}
+
+		return FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Prepares attachment string
+	 *
+	 * @param	string	$body		Message body to append to
+	 * @param	string	$boundary	Multipart boundary
+	 * @param	string	$multipart	When provided, only attachments of this type will be processed
+	 * @return	string
+	 */
+	protected function _append_attachments(&$body, $boundary, $multipart = null)
+	{
+		for ($i = 0, $c = count($this->_attachments); $i < $c; $i++)
+		{
+			if (isset($multipart) && $this->_attachments[$i]['multipart'] !== $multipart)
+			{
+				continue;
+			}
+
+			$name = isset($this->_attachments[$i]['name'][1])
+				? $this->_attachments[$i]['name'][1]
+				: basename($this->_attachments[$i]['name'][0]);
+
+			$body .= '--'.$boundary.$this->newline
+				.'Content-Type: '.$this->_attachments[$i]['type'].'; name="'.$name.'"'.$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)
+				.$this->newline
+				.$this->_attachments[$i]['content'].$this->newline;
+		}
+
+		// $name won't be set if no attachments were appended,
+		// and therefore a boundary wouldn't be necessary
+		empty($name) OR $body .= '--'.$boundary.'--';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * 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)
+	{
+		// ASCII code numbers for "safe" characters that can always be
+		// used literally, without encoding, as described in RFC 2049.
+		// http://www.ietf.org/rfc/rfc2049.txt
+		static $ascii_safe_chars = array(
+			// ' (  )   +   ,   -   .   /   :   =   ?
+			39, 40, 41, 43, 44, 45, 46, 47, 58, 61, 63,
+			// numbers
+			48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
+			// upper-case letters
+			65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
+			// lower-case letters
+			97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122
+		);
+
+		// 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")
+		{
+			return quoted_printable_encode($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 = self::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 ($ascii === 32 OR $ascii === 9)
+				{
+					if ($i === ($length - 1))
+					{
+						$char = $escape.sprintf('%02s', dechex($ascii));
+					}
+				}
+				// DO NOT move this below the $ascii_safe_chars line!
+				//
+				// = (equals) signs are allowed by RFC2049, but must be encoded
+				// as they are the encoding delimiter!
+				elseif ($ascii === 61)
+				{
+					$char = $escape.strtoupper(sprintf('%02s', dechex($ascii)));  // =3D
+				}
+				elseif ( ! in_array($ascii, $ascii_safe_chars, TRUE))
+				{
+					$char = $escape.strtoupper(sprintf('%02s', dechex($ascii)));
+				}
+
+				// If we're at the character limit, add the line to the output,
+				// reset our temp variable, and keep on chuggin'
+				if ((self::strlen($temp) + self::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 self::substr($output, 0, self::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')
+		{
+			// Note: We used to have mb_encode_mimeheader() as the first choice
+			//       here, but it turned out to be buggy and unreliable. DO NOT
+			//       re-add it! -- Narf
+			if (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 self::substr($output, 2);
+				}
+
+				$chars = iconv_strlen($str, 'UTF-8');
+			}
+			elseif (MB_ENABLED === TRUE)
+			{
+				$chars = mb_strlen($str, 'UTF-8');
+			}
+		}
+
+		// We might already have this set for UTF-8
+		isset($chars) OR $chars = self::strlen($str);
+
+		$output = '=?'.$this->charset.'?Q?';
+		for ($i = 0, $length = self::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 = self::strlen($chr)) > 74)
+			{
+				$output .= '?='.$this->crlf // EOL
+					.' =?'.$this->charset.'?Q?'.$chr; // New line
+				$length = 6 + self::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[] = self::substr($set, 1);
+				$float += $this->bcc_batch_size;
+				$set = '';
+			}
+
+			if ($i === $c-1)
+			{
+				$chunk[] = self::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();
+
+		$protocol = $this->_get_protocol();
+		$method   = '_send_with_'.$protocol;
+		if ( ! $this->$method())
+		{
+			$this->_set_error_message('lang:email_send_failure_'.($protocol === 'mail' ? 'phpmail' : $protocol));
+			return FALSE;
+		}
+
+		$this->_set_error_message('lang:email_sent', $protocol);
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate email for shell
+	 *
+	 * Applies stricter, shell-safe validation to email addresses.
+	 * Introduced to prevent RCE via sendmail's -f option.
+	 *
+	 * @see	https://github.com/bcit-ci/CodeIgniter/issues/4963
+	 * @see	https://gist.github.com/Zenexer/40d02da5e07f151adeaeeaa11af9ab36
+	 * @license	https://creativecommons.org/publicdomain/zero/1.0/	CC0 1.0, Public Domain
+	 *
+	 * Credits for the base concept go to Paul Buonopane <paul@namepros.com>
+	 *
+	 * @param	string	$email
+	 * @return	bool
+	 */
+	protected function _validate_email_for_shell(&$email)
+	{
+		if (function_exists('idn_to_ascii') && strpos($email, '@'))
+		{
+			list($account, $domain) = explode('@', $email, 2);
+			$domain = defined('INTL_IDNA_VARIANT_UTS46')
+				? idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46)
+				: idn_to_ascii($domain);
+
+			if ($domain !== FALSE)
+			{
+				$email = $account.'@'.$domain;
+			}
+		}
+
+		return (filter_var($email, FILTER_VALIDATE_EMAIL) === $email && preg_match('#\A[a-z0-9._+-]+@[a-z0-9.-]{1,253}\z#i', $email));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send using mail()
+	 *
+	 * @return	bool
+	 */
+	protected function _send_with_mail()
+	{
+		if (is_array($this->_recipients))
+		{
+			$this->_recipients = implode(', ', $this->_recipients);
+		}
+
+		// _validate_email_for_shell() below accepts by reference,
+		// so this needs to be assigned to a variable
+		$from = $this->clean_email($this->_headers['Return-Path']);
+
+		if ($this->_safe_mode === TRUE || ! $this->_validate_email_for_shell($from))
+		{
+			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 '.$from);
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send using Sendmail
+	 *
+	 * @return	bool
+	 */
+	protected function _send_with_sendmail()
+	{
+		// _validate_email_for_shell() below accepts by reference,
+		// so this needs to be assigned to a variable
+		$from = $this->clean_email($this->_headers['From']);
+		if ($this->_validate_email_for_shell($from))
+		{
+			$from = '-f '.$from;
+		}
+		else
+		{
+			$from = '';
+		}
+
+		// is popen() enabled?
+		if ( ! function_usable('popen')	OR FALSE === ($fp = @popen($this->mailpath.' -oi '.$from.' -t', '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'])))
+		{
+			$this->_smtp_end();
+			return FALSE;
+		}
+
+		foreach ($this->_recipients as $val)
+		{
+			if ( ! $this->_send_command('to', $val))
+			{
+				$this->_smtp_end();
+				return FALSE;
+			}
+		}
+
+		if (count($this->_cc_array) > 0)
+		{
+			foreach ($this->_cc_array as $val)
+			{
+				if ($val !== '' && ! $this->_send_command('to', $val))
+				{
+					$this->_smtp_end();
+					return FALSE;
+				}
+			}
+		}
+
+		if (count($this->_bcc_array) > 0)
+		{
+			foreach ($this->_bcc_array as $val)
+			{
+				if ($val !== '' && ! $this->_send_command('to', $val))
+				{
+					$this->_smtp_end();
+					return FALSE;
+				}
+			}
+		}
+
+		if ( ! $this->_send_command('data'))
+		{
+			$this->_smtp_end();
+			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);
+
+		$this->_smtp_end();
+
+		if (strpos($reply, '250') !== 0)
+		{
+			$this->_set_error_message('lang:email_smtp_error', $reply);
+			return FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * SMTP End
+	 *
+	 * Shortcut to send RSET or QUIT depending on keep-alive
+	 *
+	 * @return	void
+	 */
+	protected function _smtp_end()
+	{
+		($this->smtp_keepalive)
+			? $this->_send_command('reset')
+			: $this->_send_command('quit');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * 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');
+
+			/**
+			 * STREAM_CRYPTO_METHOD_TLS_CLIENT is quite the mess ...
+			 *
+			 * - On PHP <5.6 it doesn't even mean TLS, but SSL 2.0, and there's no option to use actual TLS
+			 * - On PHP 5.6.0-5.6.6, >=7.2 it means negotiation with any of TLS 1.0, 1.1, 1.2
+			 * - On PHP 5.6.7-7.1.* it means only TLS 1.0
+			 *
+			 * We want the negotiation, so we'll force it below ...
+			 */
+			$method = is_php('5.6')
+				? STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
+				: STREAM_CRYPTO_METHOD_TLS_CLIENT;
+			$crypto = stream_socket_enable_crypto($this->_smtp_connect, TRUE, $method);
+
+			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	bool
+	 */
+	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) self::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;
+		}
+
+		if ($this->smtp_keepalive)
+		{
+			$this->_smtp_auth = FALSE;
+		}
+
+		return TRUE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Send SMTP data
+	 *
+	 * @param	string	$data
+	 * @return	bool
+	 */
+	protected function _send_data($data)
+	{
+		$data .= $this->newline;
+		for ($written = $timestamp = 0, $length = self::strlen($data); $written < $length; $written += $result)
+		{
+			if (($result = fwrite($this->_smtp_connect, self::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;
+			}
+
+			$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';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Destructor
+	 *
+	 * @return	void
+	 */
+	public function __destruct()
+	{
+		is_resource($this->_smtp_connect) && $this->_send_command('quit');
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Byte-safe strlen()
+	 *
+	 * @param	string	$str
+	 * @return	int
+	 */
+	protected static function strlen($str)
+	{
+		return (self::$func_overload)
+			? 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_overload)
+		{
+			// 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/system/libraries/Encrypt.php b/system/libraries/Encrypt.php
new file mode 100644
index 0000000..4d1dae5
--- /dev/null
+++ b/system/libraries/Encrypt.php
@@ -0,0 +1,522 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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 ( ! self::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 https://codeigniter.com/userguide3/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 = self::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 = self::strlen($string), $lh = self::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_DEV_URANDOM);
+		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 > self::strlen($data))
+		{
+			return FALSE;
+		}
+
+		$init_vect = self::substr($data, 0, $init_size);
+		$data      = self::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 = self::strlen($data), $lk = self::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 = self::strlen($data), $lk = self::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);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Byte-safe strlen()
+	 *
+	 * @param	string	$str
+	 * @return	int
+	 */
+	protected static function strlen($str)
+	{
+		return defined('MB_OVERLOAD_STRING')
+			? 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 (defined('MB_OVERLOAD_STRING'))
+		{
+			// 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/system/libraries/Encryption.php b/system/libraries/Encryption.php
new file mode 100644
index 0000000..a1ad870
--- /dev/null
+++ b/system/libraries/Encryption.php
@@ -0,0 +1,942 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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_overload flag
+	 *
+	 * @var	bool
+	 */
+	protected static $func_overload;
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params	Configuration parameters
+	 * @return	void
+	 */
+	public function __construct(array $params = array())
+	{
+		$this->_drivers = array(
+			'mcrypt'  => defined('MCRYPT_DEV_URANDOM'),
+			'openssl' => extension_loaded('openssl')
+		);
+
+		if ( ! $this->_drivers['mcrypt'] && ! $this->_drivers['openssl'])
+		{
+			show_error('Encryption: Unable to find an available encryption driver.');
+		}
+
+		isset(self::$func_overload) OR self::$func_overload = ( ! is_php('8.0') && extension_loaded('mbstring') && @ini_get('mbstring.func_overload'));
+		$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)
+	{
+		if (function_exists('random_bytes'))
+		{
+			try
+			{
+				return random_bytes((int) $length);
+			}
+			catch (Exception $e)
+			{
+				log_message('error', $e->getMessage());
+				return FALSE;
+			}
+		}
+		elseif (defined('MCRYPT_DEV_URANDOM'))
+		{
+			return mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
+		}
+
+		$is_secure = NULL;
+		$key = openssl_random_pseudo_bytes($length, $is_secure);
+		return ($is_secure === TRUE)
+			? $key
+			: FALSE;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * 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)
+			? $this->create_key($iv_size)
+			: 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']))
+			? $this->create_key($iv_size)
+			: '';
+
+		$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 = '';
+		}
+
+		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 = '';
+		}
+
+		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;
+			}
+
+			$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	int
+	 */
+	protected static function strlen($str)
+	{
+		return (self::$func_overload)
+			? mb_strlen((string) $str, '8bit')
+			: strlen((string) $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_overload)
+		{
+			// 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/system/libraries/Form_validation.php b/system/libraries/Form_validation.php
new file mode 100644
index 0000000..024f0ed
--- /dev/null
+++ b/system/libraries/Form_validation.php
@@ -0,0 +1,1599 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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 = '')
+	{
+		$validation_array = empty($this->validation_data)
+			? $_POST
+			: $this->validation_data;
+
+		// 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'], $row['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
+		empty($this->validation_data) && $this->_reset_post_array();
+
+		return ($total_errors === 0);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Prepare rules
+	 *
+	 * Re-orders the provided rules in order of importance, so that
+	 * they can easily be executed later without weird checks ...
+	 *
+	 * "Callbacks" are given the highest priority (always called),
+	 * followed by 'required' (called if callbacks didn't fail),
+	 * and then every next rule depends on the previous one passing.
+	 *
+	 * @param	array	$rules
+	 * @return	array
+	 */
+	protected function _prepare_rules($rules)
+	{
+		$new_rules = array();
+		$callbacks = array();
+
+		foreach ($rules as &$rule)
+		{
+			// Let 'required' always be the first (non-callback) rule
+			if ($rule === 'required')
+			{
+				array_unshift($new_rules, 'required');
+			}
+			// 'isset' is a kind of a weird alias for 'required' ...
+			elseif ($rule === 'isset' && (empty($new_rules) OR $new_rules[0] !== 'required'))
+			{
+				array_unshift($new_rules, 'isset');
+			}
+			// The old/classic 'callback_'-prefixed rules
+			elseif (is_string($rule) && strncmp('callback_', $rule, 9) === 0)
+			{
+				$callbacks[] = $rule;
+			}
+			// Proper callables
+			elseif (is_callable($rule))
+			{
+				$callbacks[] = $rule;
+			}
+			// "Named" callables; i.e. array('name' => $callable)
+			elseif (is_array($rule) && isset($rule[0], $rule[1]) && is_callable($rule[1]))
+			{
+				$callbacks[] = $rule;
+			}
+			// Everything else goes at the end of the queue
+			else
+			{
+				$new_rules[] = $rule;
+			}
+		}
+
+		return array_merge($callbacks, $new_rules);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * 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)
+				{
+					isset($_POST[$field]) && $_POST[$field] = is_array($row['postdata']) ? NULL : $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];
+						}
+					}
+
+					$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
+		//
+		// Note: We MUST check if the array is empty or not!
+		//       Otherwise empty arrays will always pass validation.
+		if (is_array($postdata) && ! empty($postdata))
+		{
+			foreach ($postdata as $key => $val)
+			{
+				$this->_execute($row, $rules, $val, $key);
+			}
+
+			return;
+		}
+
+		$rules = $this->_prepare_rules($rules);
+		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];
+			}
+
+			// Ignore empty, non-required inputs with a few exceptions ...
+			if (
+				($postdata === NULL OR $postdata === '')
+				&& $callback === FALSE
+				&& $callable === FALSE
+				&& ! in_array($rule, array('required', 'isset', 'matches'), TRUE)
+			)
+			{
+				continue;
+			}
+
+			// 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;
+				}
+			}
+			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)';
+				}
+				else
+				{
+					$line = $this->_get_error_message($rule, $row['field']);
+				}
+
+				// 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;
+			}
+		}
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Get the error message for the rule
+	 *
+	 * @param 	string $rule 	The rule name
+	 * @param 	string $field	The field name
+	 * @return 	string
+	 */
+	protected function _get_error_message($rule, $field)
+	{
+		// check if a custom message is defined through validation config row.
+		if (isset($this->_field_data[$field]['errors'][$rule]))
+		{
+			return $this->_field_data[$field]['errors'][$rule];
+		}
+		// check if a custom message has been set using the set_message() function
+		elseif (isset($this->_error_messages[$rule]))
+		{
+			return $this->_error_messages[$rule];
+		}
+		elseif (FALSE !== ($line = $this->CI->lang->line('form_validation_'.$rule)))
+		{
+			return $line;
+		}
+		// DEPRECATED support for non-prefixed keys, lang file again
+		elseif (FALSE !== ($line = $this->CI->lang->line($rule, FALSE)))
+		{
+			return $line;
+		}
+
+		return $this->CI->lang->line('form_validation_error_message_not_set').'('.$rule.')';
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * 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)
+			? (empty($str) === FALSE)
+			: (trim((string) $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(strtolower($matches[1]), array('http', 'https'), TRUE))
+			{
+				return FALSE;
+			}
+
+			$str = $matches[2];
+		}
+
+		// Apparently, FILTER_VALIDATE_URL doesn't reject digit-only names for some reason ...
+		// See https://github.com/bcit-ci/CodeIgniter/issues/5755
+		if (ctype_digit($str))
+		{
+			return FALSE;
+		}
+
+		// PHP 7 accepts IPv6 addresses within square brackets as hostnames,
+		// but it appears that the PR that came in with https://bugs.php.net/bug.php?id=68039
+		// was never merged into a PHP 5 branch ... https://3v4l.org/8PsSN
+		if (preg_match('/^\[([^\]]+)\]/', $str, $matches) && ! is_php('7') && filter_var($matches[1], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== FALSE)
+		{
+			$str = 'ipv6.host'.substr($str, strlen($matches[1]) + 2);
+		}
+
+		return (filter_var('http://'.$str, FILTER_VALIDATE_URL) !== FALSE);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Valid Email
+	 *
+	 * @param	string
+	 * @return	bool
+	 */
+	public function valid_email($str)
+	{
+		if (function_exists('idn_to_ascii') && preg_match('#\A([^@]+)@(.+)\z#', $str, $matches))
+		{
+			$domain = defined('INTL_IDNA_VARIANT_UTS46')
+				? idn_to_ascii($matches[2], 0, INTL_IDNA_VARIANT_UTS46)
+				: idn_to_ascii($matches[2]);
+
+			if ($domain !== FALSE)
+			{
+				$str = $matches[1].'@'.$domain;
+			}
+		}
+
+		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.
+	 *
+	 * @deprecated	3.0.6	Not used anywhere within the framework and pretty much useless
+	 * @param	mixed	$data	Input data
+	 * @return	mixed
+	 */
+	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/system/libraries/Ftp.php b/system/libraries/Ftp.php
new file mode 100644
index 0000000..15a0887
--- /dev/null
+++ b/system/libraries/Ftp.php
@@ -0,0 +1,668 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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 ($this->conn_id === FALSE)
+		{
+			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 probably 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/system/libraries/Image_lib.php b/system/libraries/Image_lib.php
new file mode 100644
index 0000000..3f9698c
--- /dev/null
+++ b/system/libraries/Image_lib.php
@@ -0,0 +1,1843 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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);
+		}
+
+		/**
+		 * A work-around for some improperly formatted, but
+		 * usable JPEGs; known to be produced by Samsung
+		 * smartphones' front-facing cameras.
+		 *
+		 * @see	https://github.com/bcit-ci/CodeIgniter/issues/4967
+		 * @see	https://bugs.php.net/bug.php?id=72404
+		 */
+		ini_set('gd.jpeg_ignore_warning', 1);
+
+		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'), TRUE))
+					{
+						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;
+						}
+					}
+					elseif (in_array($key, array('width', 'height'), TRUE) && ! ctype_digit((string) $val))
+					{
+						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 && strpos($this->new_image, '\\') === FALSE)
+		{
+			$this->dest_image  = $this->new_image;
+			$this->dest_folder = $this->source_folder;
+		}
+		else
+		{
+			// Is there a file name?
+			if ( ! preg_match('#\.(jpg|jpeg|gif|png)$#i', $this->new_image))
+			{
+				$this->dest_image  = $this->source_image;
+				$this->dest_folder = $this->new_image;
+			}
+			else
+			{
+				$x = explode('/', str_replace('\\', '/', $this->new_image));
+				$this->dest_image  = end($x);
+				$this->dest_folder = str_replace($this->dest_image, '', $this->new_image);
+			}
+
+			$this->dest_folder = realpath($this->dest_folder).'/';
+		}
+
+		/* 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);
+
+		if ($this->dynamic_output !== TRUE)
+		{
+			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;
+		}
+		elseif ($action === 'rotate')
+		{
+			$cmd .= ($this->rotation_angle === 'hor' OR $this->rotation_angle === 'vrt')
+					? ' -flop'
+					: ' -rotate '.$this->rotation_angle;
+		}
+		else // Resize
+		{
+			if($this->maintain_ratio === TRUE)
+			{
+				$cmd .= ' -resize '.$this->width.'x'.$this->height;
+			}
+			else
+			{
+				$cmd .= ' -resize '.$this->width.'x'.$this->height.'\!';
+			}
+		}
+
+		$cmd .= ' '.escapeshellarg($this->full_src_path).' '.escapeshellarg($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.' '.escapeshellarg($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);
+		if ($vals === FALSE)
+		{
+			$this->set_error('imglib_invalid_image');
+			return FALSE;
+		}
+
+		$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/system/libraries/Javascript.php b/system/libraries/Javascript.php
new file mode 100644
index 0000000..8f2cf58
--- /dev/null
+++ b/system/libraries/Javascript.php
@@ -0,0 +1,857 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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/system/libraries/Javascript/Jquery.php b/system/libraries/Javascript/Jquery.php
new file mode 100644
index 0000000..e06f1ba
--- /dev/null
+++ b/system/libraries/Javascript/Jquery.php
@@ -0,0 +1,1077 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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	https://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/system/libraries/Javascript/index.html b/system/libraries/Javascript/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/libraries/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/system/libraries/Migration.php b/system/libraries/Migration.php
new file mode 100644
index 0000000..9ee92b6
--- /dev/null
+++ b/system/libraries/Migration.php
@@ -0,0 +1,478 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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 string
+	 */
+	protected $_migration_regex;
+
+	/**
+	 * 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)
+		{
+			$method = 'up';
+		}
+		elseif ($target_version < $current_version)
+		{
+			$method = 'down';
+			// We need this so that migrations are applied in reverse order
+			krsort($migrations);
+		}
+		else
+		{
+			// Well, there's nothing to migrate then ...
+			return TRUE;
+		}
+
+		// Validate all available migrations within our target range.
+		//
+		// Unfortunately, we'll have to use another loop to run them
+		// in order to avoid leaving the procedure in a broken state.
+		//
+		// See https://github.com/bcit-ci/CodeIgniter/issues/4539
+		$pending = array();
+		foreach ($migrations as $number => $file)
+		{
+			// Ignore versions out of our range.
+			//
+			// Because we've previously sorted the $migrations array depending on the direction,
+			// we can safely break the loop once we reach $target_version ...
+			if ($method === 'up')
+			{
+				if ($number <= $current_version)
+				{
+					continue;
+				}
+				elseif ($number > $target_version)
+				{
+					break;
+				}
+			}
+			else
+			{
+				if ($number > $current_version)
+				{
+					continue;
+				}
+				elseif ($number <= $target_version)
+				{
+					break;
+				}
+			}
+
+			// Check for sequence gaps
+			if ($this->_migration_type === 'sequential')
+			{
+				if (isset($previous) && abs($number - $previous) > 1)
+				{
+					$this->_error_string = sprintf($this->lang->line('migration_sequence_gap'), $number);
+					return FALSE;
+				}
+
+				$previous = $number;
+			}
+
+			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;
+			}
+			elseif ( ! method_exists($class, $method) OR ! (new ReflectionMethod($class, $method))->isPublic())
+			{
+				$this->_error_string = sprintf($this->lang->line('migration_missing_'.$method.'_method'), $class);
+				return FALSE;
+			}
+
+			$pending[$number] = array($class, $method);
+		}
+
+		// Now just run the necessary migrations
+		foreach ($pending as $number => $migration)
+		{
+			log_message('debug', 'Migrating '.$method.' from version '.$current_version.' to version '.$number);
+
+			$migration[0] = new $migration[0];
+			call_user_func($migration);
+			$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/system/libraries/Pagination.php b/system/libraries/Pagination.php
new file mode 100644
index 0000000..4d945a0
--- /dev/null
+++ b/system/libraries/Pagination.php
@@ -0,0 +1,705 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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;
+			}
+		}
+
+		// _parse_attributes(), called by initialize(), needs to run at least once
+		// in order to enable "rel" attributes, and this triggers it.
+		isset($params['attributes']) OR $params['attributes'] = array();
+
+		$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())
+	{
+		if (isset($params['attributes']) && 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 control, 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);
+		}
+		elseif (empty($this->cur_page))
+		{
+			// 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);
+			}
+		}
+		else
+		{
+			$this->cur_page = (string) $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	https://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/system/libraries/Parser.php b/system/libraries/Parser.php
new file mode 100644
index 0000000..e0adec6
--- /dev/null
+++ b/system/libraries/Parser.php
@@ -0,0 +1,249 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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/system/libraries/Profiler.php b/system/libraries/Profiler.php
new file mode 100644
index 0000000..d423c14
--- /dev/null
+++ b/system/libraries/Profiler.php
@@ -0,0 +1,575 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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 = "'".htmlspecialchars($key, ENT_QUOTES, config_item('charset'))."'";
+				$val = (is_array($val) OR is_object($val))
+					? '<pre>'.htmlspecialchars(print_r($val, TRUE), ENT_QUOTES, config_item('charset')).'</pre>'
+					: htmlspecialchars($val, ENT_QUOTES, config_item('charset'));
+
+				$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;">'
+					.$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 = "'".htmlspecialchars($key, ENT_QUOTES, config_item('charset'))."'";
+				$val = (is_array($val) OR is_object($val))
+					? '<pre>'.htmlspecialchars(print_r($val, TRUE), ENT_QUOTES, config_item('charset')).'</pre>'
+					: htmlspecialchars($val, ENT_QUOTES, config_item('charset'));
+
+				$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;">'
+					.$val."</td></tr>\n";
+			}
+
+			foreach ($_FILES as $key => $val)
+			{
+				is_int($key) OR $key = "'".htmlspecialchars($key, ENT_QUOTES, config_item('charset'))."'";
+				$val = (is_array($val) OR is_object($val))
+					? '<pre>'.htmlspecialchars(print_r($val, TRUE), ENT_QUOTES, config_item('charset')).'</pre>'
+					: htmlspecialchars($val, ENT_QUOTES, config_item('charset'));
+
+				$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;">'
+					.$val."</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]) ? htmlspecialchars($_SERVER[$header], ENT_QUOTES, config_item('charset')) : '';
+			$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)
+		{
+			$pre       = '';
+			$pre_close = '';
+
+			if (is_array($val) OR is_object($val))
+			{
+				$val = print_r($val, TRUE);
+
+				$pre       = '<pre>' ;
+				$pre_close = '</pre>';
+			}
+
+			$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;">'.$pre.htmlspecialchars((string) $val, ENT_QUOTES, config_item('charset')).$pre_close."</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)
+		{
+			$pre       = '';
+			$pre_close = '';
+
+			if (is_array($val) OR is_object($val))
+			{
+				$val = print_r($val, TRUE);
+
+				$pre       = '<pre>' ;
+				$pre_close = '</pre>';
+			}
+
+			$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;">'.$pre.htmlspecialchars((string) $val, ENT_QUOTES, config_item('charset')).$pre_close."</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/system/libraries/Session/CI_Session_driver_interface.php b/system/libraries/Session/CI_Session_driver_interface.php
new file mode 100644
index 0000000..23a0dfd
--- /dev/null
+++ b/system/libraries/Session/CI_Session_driver_interface.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2022, CodeIgniter Foundation
+ *
+ * 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) 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * CI_Session_driver_interface
+ *
+ * A compatibility typeless SessionHandlerInterface alias
+ *
+ * @package	CodeIgniter
+ * @subpackage	Libraries
+ * @category	Sessions
+ * @author	Andrey Andreev
+ * @link	https://codeigniter.com/userguide3/libraries/sessions.html
+ */
+interface CI_Session_driver_interface {
+
+	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);
+	public function updateTimestamp($session_id, $data);
+	public function validateId($session_id);
+}
diff --git a/system/libraries/Session/OldSessionWrapper.php b/system/libraries/Session/OldSessionWrapper.php
new file mode 100644
index 0000000..d013c77
--- /dev/null
+++ b/system/libraries/Session/OldSessionWrapper.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2022, CodeIgniter Foundation
+ *
+ * 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) 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * OldSessionWrapper
+ *
+ * PHP 8 Session handler compatibility wrapper, pre-PHP8 version
+ *
+ * @package	CodeIgniter
+ * @subpackage	Libraries
+ * @category	Sessions
+ * @author	Andrey Andreev
+ * @link	https://codeigniter.com/userguide3/libraries/sessions.html
+ */
+class CI_SessionWrapper implements SessionHandlerInterface, SessionUpdateTimestampHandlerInterface {
+
+	protected $driver;
+
+	public function __construct(CI_Session_driver_interface $driver)
+	{
+		$this->driver = $driver;
+	}
+
+	public function open($save_path, $name)
+	{
+		return $this->driver->open($save_path, $name);
+	}
+
+	public function close()
+	{
+		return $this->driver->close();
+	}
+
+	public function read($id)
+	{
+		return $this->driver->read($id);
+	}
+
+	public function write($id, $data)
+	{
+		return $this->driver->write($id, $data);
+	}
+
+	public function destroy($id)
+	{
+		return $this->driver->destroy($id);
+	}
+
+	public function gc($maxlifetime)
+	{
+		return $this->driver->gc($maxlifetime);
+	}
+
+	public function updateTimestamp($id, $data)
+	{
+		return $this->driver->updateTimestamp($id, $data);
+	}
+
+	public function validateId($id)
+	{
+		return $this->driver->validateId($id);
+	}
+}
diff --git a/system/libraries/Session/PHP8SessionWrapper.php b/system/libraries/Session/PHP8SessionWrapper.php
new file mode 100644
index 0000000..41889bc
--- /dev/null
+++ b/system/libraries/Session/PHP8SessionWrapper.php
@@ -0,0 +1,100 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2022, CodeIgniter Foundation
+ *
+ * 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) 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PHP8SessionWrapper
+ *
+ * PHP 8 Session handler compatibility wrapper
+ *
+ * @package	CodeIgniter
+ * @subpackage	Libraries
+ * @category	Sessions
+ * @author	Andrey Andreev
+ * @link	https://codeigniter.com/userguide3/libraries/sessions.html
+ */
+class CI_SessionWrapper implements SessionHandlerInterface, SessionUpdateTimestampHandlerInterface {
+
+	protected CI_Session_driver_interface $driver;
+
+	public function __construct(CI_Session_driver_interface $driver)
+	{
+		$this->driver = $driver;
+	}
+
+	public function open(string $save_path, string $name): bool
+	{
+		return $this->driver->open($save_path, $name);
+	}
+
+	public function close(): bool
+	{
+		return $this->driver->close();
+	}
+
+	#[\ReturnTypeWillChange]
+	public function read(string $id): mixed
+	{
+		return $this->driver->read($id);
+	}
+
+	public function write(string $id, string $data): bool
+	{
+		return $this->driver->write($id, $data);
+	}
+
+	public function destroy(string $id): bool
+	{
+		return $this->driver->destroy($id);
+	}
+
+	#[\ReturnTypeWillChange]
+	public function gc(int $maxlifetime): mixed
+	{
+		return $this->driver->gc($maxlifetime);
+	}
+
+	public function updateTimestamp(string $id, string$data): bool
+	{
+		return $this->driver->updateTimestamp($id, $data);
+	}
+
+	public function validateId(string $id): bool
+	{
+		return $this->driver->validateId($id);
+	}
+}
diff --git a/system/libraries/Session/Session.php b/system/libraries/Session/Session.php
new file mode 100644
index 0000000..a211ce3
--- /dev/null
+++ b/system/libraries/Session/Session.php
@@ -0,0 +1,1032 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/libraries/sessions.html
+ */
+class CI_Session {
+
+	/**
+	 * Userdata array
+	 *
+	 * Just a reference to $_SESSION, for BC purposes.
+	 */
+	public $userdata;
+
+	protected $_driver = 'files';
+	protected $_config;
+	protected $_sid_regexp;
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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'))
+		{
+			log_message('debug', 'Session: "sess_driver" is empty; using BC fallback to "sess_use_database".');
+			$this->_driver = 'database';
+		}
+
+		$class = $this->_ci_load_classes($this->_driver);
+
+		// Configuration ...
+		$this->_configure($params);
+		$this->_config['_sid_regexp'] = $this->_sid_regexp;
+
+		$class   = new $class($this->_config);
+		$wrapper = new CI_SessionWrapper($class);
+		if (is_php('5.4'))
+		{
+			session_set_save_handler($wrapper, TRUE);
+		}
+		else
+		{
+			session_set_save_handler(
+				array($wrapper, 'open'),
+				array($wrapper, 'close'),
+				array($wrapper, 'read'),
+				array($wrapper, 'write'),
+				array($wrapper, 'destroy'),
+				array($wrapper, 'gc')
+			);
+
+			register_shutdown_function('session_write_close');
+		}
+
+		// 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('#\A'.$this->_sid_regexp.'\z#', $_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())
+		{
+			$expires = empty($this->_config['cookie_lifetime']) ? 0 : time() + $this->_config['cookie_lifetime'];
+			if (is_php('7.3'))
+			{
+				setcookie(
+					$this->_config['cookie_name'],
+					session_id(),
+					array(
+						'expires' => $expires,
+						'path' => $this->_config['cookie_path'],
+						'domain' => $this->_config['cookie_domain'],
+						'secure' => $this->_config['cookie_secure'],
+						'httponly' => TRUE,
+						'samesite' => $this->_config['cookie_samesite']
+					)
+				);
+			}
+			else
+			{
+				$header = 'Set-Cookie: '.$this->_config['cookie_name'].'='.session_id();
+				$header .= empty($expires) ? '' : '; Expires='.gmdate('D, d-M-Y H:i:s T', $expires).'; Max-Age='.$this->_config['cookie_lifetime'];
+				$header .= '; Path='.$this->_config['cookie_path'];
+				$header .= ($this->_config['cookie_domain'] !== '' ? '; Domain='.$this->_config['cookie_domain'] : '');
+				$header .= ($this->_config['cookie_secure'] ? '; Secure' : '').'; HttpOnly; SameSite='.$this->_config['cookie_samesite'];
+				header($header);
+			}
+
+			if ( ! $this->_config['cookie_secure'] && $this->_config['cookie_samesite'] === 'None')
+			{
+				log_message('error', "Session: '".$this->_config['cookie_name']."' cookie sent with SameSite=None, but without Secure attribute.'");
+			}
+		}
+
+		$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');
+		// PHP 7 compatibility
+		interface_exists('SessionUpdateTimestampHandlerInterface', FALSE) OR require_once(BASEPATH.'libraries/Session/SessionUpdateTimestampHandlerInterface.php');
+
+		require_once(BASEPATH.'libraries/Session/CI_Session_driver_interface.php');
+		$wrapper = is_php('8.0') ? 'PHP8SessionWrapper' : 'OldSessionWrapper';
+		require_once(BASEPATH.'libraries/Session/'.$wrapper.'.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, FALSE) && file_exists($file_path = APPPATH.'libraries/Session/drivers/'.$prefix.$class.'.php'))
+		{
+			require_once($file_path);
+			if (class_exists($prefix.$class, FALSE))
+			{
+				return $prefix.$class;
+			}
+
+			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');
+
+		isset($params['cookie_samesite']) OR $params['cookie_samesite'] = config_item('sess_samesite');
+		if ( ! isset($params['cookie_samesite']) && is_php('7.3'))
+		{
+			$params['cookie_samesite'] = ini_get('session.cookie_samesite');
+		}
+
+		if (isset($params['cookie_samesite']))
+		{
+			$params['cookie_samesite'] = ucfirst(strtolower($params['cookie_samesite']));
+			in_array($params['cookie_samesite'], array('Lax', 'Strict', 'None'), TRUE) OR $params['cookie_samesite'] = 'Lax';
+		}
+		else
+		{
+			$params['cookie_samesite'] = 'Lax';
+		}
+
+		if (is_php('7.3'))
+		{
+			session_set_cookie_params(array(
+				'lifetime' => $params['cookie_lifetime'],
+				'path'     => $params['cookie_path'],
+				'domain'   => $params['cookie_domain'],
+				'secure'   => $params['cookie_secure'],
+				'httponly' => TRUE,
+				'samesite' => $params['cookie_samesite']
+			));
+		}
+		else
+		{
+			session_set_cookie_params(
+				$params['cookie_lifetime'],
+				$params['cookie_path'].'; SameSite='.$params['cookie_samesite'],
+				$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);
+
+		$this->_configure_sid_length();
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Configure session ID length
+	 *
+	 * To make life easier, we used to force SHA-1 and 4 bits per
+	 * character on everyone. And of course, someone was unhappy.
+	 *
+	 * Then PHP 7.1 broke backwards-compatibility because ext/session
+	 * is such a mess that nobody wants to touch it with a pole stick,
+	 * and the one guy who does, nobody has the energy to argue with.
+	 *
+	 * So we were forced to make changes, and OF COURSE something was
+	 * going to break and now we have this pile of shit. -- Narf
+	 *
+	 * @return	void
+	 */
+	protected function _configure_sid_length()
+	{
+		if (PHP_VERSION_ID < 70100)
+		{
+			$hash_function = ini_get('session.hash_function');
+			if (ctype_digit($hash_function))
+			{
+				if ($hash_function !== '1')
+				{
+					ini_set('session.hash_function', 1);
+				}
+
+				$bits = 160;
+			}
+			elseif ( ! in_array($hash_function, hash_algos(), TRUE))
+			{
+				ini_set('session.hash_function', 1);
+				$bits = 160;
+			}
+			elseif (($bits = strlen(hash($hash_function, 'dummy', false)) * 4) < 160)
+			{
+				ini_set('session.hash_function', 1);
+				$bits = 160;
+			}
+
+			$bits_per_character = (int) ini_get('session.hash_bits_per_character');
+			$sid_length         = (int) ceil($bits / $bits_per_character);
+		}
+		else
+		{
+			$bits_per_character = (int) ini_get('session.sid_bits_per_character');
+			$sid_length         = (int) ini_get('session.sid_length');
+			if (($bits = $sid_length * $bits_per_character) < 160)
+			{
+				// Add as many more characters as necessary to reach at least 160 bits
+				$sid_length += (int) ceil((160 % $bits) / $bits_per_character);
+				ini_set('session.sid_length', $sid_length);
+			}
+		}
+
+		// Yes, 4,5,6 are the only known possible values as of 2016-10-27
+		switch ($bits_per_character)
+		{
+			case 4:
+				$this->_sid_regexp = '[0-9a-f]';
+				break;
+			case 5:
+				$this->_sid_regexp = '[0-9a-v]';
+				break;
+			case 6:
+				$this->_sid_regexp = '[0-9a-zA-Z,-]';
+				break;
+		}
+
+		$this->_sid_regexp .= '{'.$sid_length.'}';
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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';
+				}
+				elseif ($value === 'old' || $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 temp
+	 *
+	 * @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;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * __isset()
+	 *
+	 * @param	string	$key	'session_id' or a session data key
+	 * @return	bool
+	 */
+	public function __isset($key)
+	{
+		if ($key === 'session_id')
+		{
+			return (session_status() === PHP_SESSION_ACTIVE);
+		}
+
+		return isset($_SESSION[$key]);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * __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
+	 *
+	 * @return	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	$key	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/system/libraries/Session/SessionHandlerInterface.php b/system/libraries/Session/SessionHandlerInterface.php
new file mode 100644
index 0000000..eadb63c
--- /dev/null
+++ b/system/libraries/Session/SessionHandlerInterface.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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	https://codeigniter.com/userguide3/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/system/libraries/Session/SessionUpdateTimestampHandlerInterface.php b/system/libraries/Session/SessionUpdateTimestampHandlerInterface.php
new file mode 100644
index 0000000..fe4a321
--- /dev/null
+++ b/system/libraries/Session/SessionUpdateTimestampHandlerInterface.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://codeigniter.com
+ * @since	Version 3.0.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * SessionUpdateTimestampHandlerInterface
+ *
+ * PHP 7 compatibility interface
+ *
+ * @package	CodeIgniter
+ * @subpackage	Libraries
+ * @category	Sessions
+ * @author	Andrey Andreev
+ * @link	https://codeigniter.com/userguide3/libraries/sessions.html
+ */
+interface SessionUpdateTimestampHandlerInterface {
+
+	public function updateTimestamp($session_id, $data);
+	public function validateId($session_id);
+}
diff --git a/system/libraries/Session/Session_driver.php b/system/libraries/Session/Session_driver.php
new file mode 100644
index 0000000..24b4b46
--- /dev/null
+++ b/system/libraries/Session/Session_driver.php
@@ -0,0 +1,202 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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	https://codeigniter.com/userguide3/libraries/sessions.html
+ */
+abstract class CI_Session_driver {
+
+	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;
+
+	/**
+	 * Success and failure return values
+	 *
+	 * Necessary due to a bug in all PHP 5 versions where return values
+	 * from userspace handlers are not handled properly. PHP 7 fixes the
+	 * bug, so we need to return different values depending on the version.
+	 *
+	 * @see	https://wiki.php.net/rfc/session.user.return-value
+	 * @var	mixed
+	 */
+	protected $_success, $_failure;
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params	Configuration parameters
+	 * @return	void
+	 */
+	public function __construct(&$params)
+	{
+		$this->_config =& $params;
+
+		if (is_php('7'))
+		{
+			$this->_success = TRUE;
+			$this->_failure = FALSE;
+		}
+		else
+		{
+			$this->_success = 0;
+			$this->_failure = -1;
+		}
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * PHP 5.x validate ID
+	 *
+	 * Enforces session.use_strict_mode
+	 *
+	 * @return	void
+	 */
+	public function php5_validate_id()
+	{
+		if ($this->_success === 0 && isset($_COOKIE[$this->_config['cookie_name']]) && ! $this->validateId($_COOKIE[$this->_config['cookie_name']]))
+		{
+			unset($_COOKIE[$this->_config['cookie_name']]);
+		}
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Cookie destroy
+	 *
+	 * Internal method to force removal of a cookie by the client
+	 * when session_destroy() is called.
+	 *
+	 * @return	bool
+	 */
+	protected function _cookie_destroy()
+	{
+		if ( ! is_php('7.3'))
+		{
+			$header = 'Set-Cookie: '.$this->_config['cookie_name'].'=';
+			$header .= '; Expires='.gmdate('D, d-M-Y H:i:s T', 1).'; Max-Age=-1';
+			$header .= '; Path='.$this->_config['cookie_path'];
+			$header .= ($this->_config['cookie_domain'] !== '' ? '; Domain='.$this->_config['cookie_domain'] : '');
+			$header .= ($this->_config['cookie_secure'] ? '; Secure' : '').'; HttpOnly; SameSite='.$this->_config['cookie_samesite'];
+			header($header);
+			return;
+		}
+
+		return setcookie(
+			$this->_config['cookie_name'],
+			'',
+			array(
+				'expires' => 1,
+				'path' => $this->_config['cookie_path'],
+				'domain' => $this->_config['cookie_domain'],
+				'secure' => $this->_config['cookie_secure'],
+				'httponly' => TRUE,
+				'samesite' => $this->_config['cookie_samesite']
+			)
+		);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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/system/libraries/Session/drivers/Session_database_driver.php b/system/libraries/Session/drivers/Session_database_driver.php
new file mode 100644
index 0000000..4b47536
--- /dev/null
+++ b/system/libraries/Session/drivers/Session_database_driver.php
@@ -0,0 +1,471 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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	https://codeigniter.com/userguide3/libraries/sessions.html
+ */
+class CI_Session_database_driver extends CI_Session_driver implements CI_Session_driver_interface {
+
+	/**
+	 * 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.
+		if ( ! isset($this->_config['save_path']) && ($this->_config['save_path'] = config_item('sess_table_name')))
+		{
+			log_message('debug', 'Session: "sess_save_path" is empty; using BC fallback to "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)
+	{
+		if (empty($this->_db->conn_id) && ! $this->_db->db_connect())
+		{
+			return $this->_failure;
+		}
+
+		$this->php5_validate_id();
+
+		return $this->_success;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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)
+		{
+			return $this->_failure;
+		}
+
+		// Prevent previous QB calls from messing with our queries
+		$this->_db->reset_query();
+
+		// 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()) OR ($result = $result->row()) === NULL)
+		{
+			// PHP7 will reuse the same SessionHandler object after
+			// ID regeneration, so we need to explicitly set this to
+			// FALSE instead of relying on the default ...
+			$this->_row_exists = FALSE;
+			$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;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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)
+	{
+		// Prevent previous QB calls from messing with our queries
+		$this->_db->reset_query();
+
+		// Was the ID regenerated?
+		if (isset($this->_session_id) && $session_id !== $this->_session_id)
+		{
+			if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
+			{
+				return $this->_failure;
+			}
+
+			$this->_row_exists = FALSE;
+			$this->_session_id = $session_id;
+		}
+		elseif ($this->_lock === FALSE)
+		{
+			return $this->_failure;
+		}
+
+		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);
+				$this->_row_exists = TRUE;
+				return $this->_success;
+			}
+
+			return $this->_failure;
+		}
+
+		$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 $this->_success;
+		}
+
+		return $this->_failure;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Close
+	 *
+	 * Releases locks
+	 *
+	 * @return	bool
+	 */
+	public function close()
+	{
+		return ($this->_lock && ! $this->_release_lock())
+			? $this->_failure
+			: $this->_success;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Destroy
+	 *
+	 * Destroys the current session.
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	bool
+	 */
+	public function destroy($session_id)
+	{
+		if ($this->_lock)
+		{
+			// Prevent previous QB calls from messing with our queries
+			$this->_db->reset_query();
+
+			$this->_db->where('id', $session_id);
+			if ($this->_config['match_ip'])
+			{
+				$this->_db->where('ip_address', $_SERVER['REMOTE_ADDR']);
+			}
+
+			if ( ! $this->_db->delete($this->_config['save_path']))
+			{
+				return $this->_failure;
+			}
+		}
+
+		if ($this->close() === $this->_success)
+		{
+			$this->_cookie_destroy();
+			return $this->_success;
+		}
+
+		return $this->_failure;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Garbage Collector
+	 *
+	 * Deletes expired sessions
+	 *
+	 * @param	int 	$maxlifetime	Maximum lifetime of sessions
+	 * @return	bool
+	 */
+	public function gc($maxlifetime)
+	{
+		// Prevent previous QB calls from messing with our queries
+		$this->_db->reset_query();
+
+		return ($this->_db->delete($this->_config['save_path'], 'timestamp < '.(time() - $maxlifetime)))
+			? $this->_success
+			: $this->_failure;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update Timestamp
+	 *
+	 * Update session timestamp without modifying data
+	 *
+	 * @param	string	$id	Session ID
+	 * @param	string	$data	Unknown & unused
+	 * @return	bool
+	 */
+	public function updateTimestamp($id, $unknown)
+	{
+		// Prevent previous QB calls from messing with our queries
+		$this->_db->reset_query();
+
+		$this->_db->where('id', $id);
+		if ($this->_config['match_ip'])
+		{
+			$this->_db->where('ip_address', $_SERVER['REMOTE_ADDR']);
+		}
+
+		return (bool) $this->_db->update($this->_config['save_path'], array('timestamp' => time()));
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate ID
+	 *
+	 * Checks whether a session ID record exists server-side,
+	 * to enforce session.use_strict_mode.
+	 *
+	 * @param	string	$id	Session ID
+	 * @return	bool
+	 */
+	public function validateId($id)
+	{
+		// Prevent previous QB calls from messing with our queries
+		$this->_db->reset_query();
+
+		$this->_db->select('1')->from($this->_config['save_path'])->where('id', $id);
+		empty($this->_config['match_ip']) OR $this->_db->where('ip_address', $_SERVER['REMOTE_ADDR']);
+		$result = $this->_db->get();
+		empty($result) OR $result = $result->row();
+
+		return ! empty($result);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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 = md5($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/system/libraries/Session/drivers/Session_files_driver.php b/system/libraries/Session/drivers/Session_files_driver.php
new file mode 100644
index 0000000..be0dc9e
--- /dev/null
+++ b/system/libraries/Session/drivers/Session_files_driver.php
@@ -0,0 +1,449 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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	https://codeigniter.com/userguide3/libraries/sessions.html
+ */
+class CI_Session_files_driver extends CI_Session_driver implements CI_Session_driver_interface {
+
+	/**
+	 * 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;
+
+	/**
+	 * Validate SID regular expression
+	 *
+	 * @var	string
+	 */
+	protected $_sid_regexp;
+
+	/**
+	 * mbstring.func_overload flag
+	 *
+	 * @var	bool
+	 */
+	protected static $func_overload;
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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
+		{
+			log_message('debug', 'Session: "sess_save_path" is empty; using "session.save_path" value from php.ini.');
+			$this->_config['save_path'] = rtrim(ini_get('session.save_path'), '/\\');
+		}
+
+		$this->_sid_regexp = $this->_config['_sid_regexp'];
+
+		isset(self::$func_overload) OR self::$func_overload = ( ! is_php('8.0') && extension_loaded('mbstring') && @ini_get('mbstring.func_overload'));
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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))
+			{
+				log_message('error', "Session: Configured save path '".$this->_config['save_path']."' is not a directory, doesn't exist or cannot be created.");
+				return $this->_failure;
+			}
+		}
+		elseif ( ! is_writable($save_path))
+		{
+			log_message('error', "Session: Configured save path '".$this->_config['save_path']."' is not writable by the PHP process.");
+			return $this->_failure;
+		}
+
+		$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']) : '');
+
+		$this->php5_validate_id();
+
+		return $this->_success;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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)
+		{
+			$this->_file_new = ! file_exists($this->_file_path.$session_id);
+
+			if (($this->_file_handle = fopen($this->_file_path.$session_id, 'c+b')) === FALSE)
+			{
+				log_message('error', "Session: Unable to open file '".$this->_file_path.$session_id."'.");
+				return $this->_failure;
+			}
+
+			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 $this->_failure;
+			}
+
+			// 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 '';
+			}
+
+			// Prevent possible data corruption
+			// See https://github.com/bcit-ci/CodeIgniter/issues/5857
+			clearstatcache(TRUE, $this->_file_path.$session_id);
+		}
+		// We shouldn't need this, but apparently we do ...
+		// See https://github.com/bcit-ci/CodeIgniter/issues/4039
+		elseif ($this->_file_handle === FALSE)
+		{
+			return $this->_failure;
+		}
+		else
+		{
+			rewind($this->_file_handle);
+		}
+
+		$session_data = '';
+		for ($read = 0, $length = filesize($this->_file_path.$session_id); $read < $length; $read += self::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() === $this->_failure OR $this->read($session_id) === $this->_failure))
+		{
+			return $this->_failure;
+		}
+
+		if ( ! is_resource($this->_file_handle))
+		{
+			return $this->_failure;
+		}
+		elseif ($this->_fingerprint === md5($session_data))
+		{
+			return ( ! $this->_file_new && ! touch($this->_file_path.$session_id))
+				? $this->_failure
+				: $this->_success;
+		}
+
+		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 $this->_failure;
+			}
+		}
+
+		$this->_fingerprint = md5($session_data);
+		return $this->_success;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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 $this->_success;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Destroy
+	 *
+	 * Destroys the current session.
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	bool
+	 */
+	public function destroy($session_id)
+	{
+		if ($this->close() === $this->_success)
+		{
+			if (file_exists($this->_file_path.$session_id))
+			{
+				$this->_cookie_destroy();
+				return unlink($this->_file_path.$session_id)
+					? $this->_success
+					: $this->_failure;
+			}
+
+			return $this->_success;
+		}
+		elseif ($this->_file_path !== NULL)
+		{
+			clearstatcache();
+			if (file_exists($this->_file_path.$session_id))
+			{
+				$this->_cookie_destroy();
+				return unlink($this->_file_path.$session_id)
+					? $this->_success
+					: $this->_failure;
+			}
+
+			return $this->_success;
+		}
+
+		return $this->_failure;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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 $this->_failure;
+		}
+
+		$ts = time() - $maxlifetime;
+
+		$pattern = ($this->_config['match_ip'] === TRUE)
+			? '[0-9a-f]{32}'
+			: '';
+
+		$pattern = sprintf(
+			'#\A%s'.$pattern.$this->_sid_regexp.'\z#',
+			preg_quote($this->_config['cookie_name'])
+		);
+
+		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 $this->_success;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update Timestamp
+	 *
+	 * Update session timestamp without modifying data
+	 *
+	 * @param	string	$id	Session ID
+	 * @param	string	$data	Unknown & unused
+	 * @return	bool
+	 */
+	public function updateTimestamp($id, $unknown)
+	{
+		return touch($this->_file_path.$id);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate ID
+	 *
+	 * Checks whether a session ID record exists server-side,
+	 * to enforce session.use_strict_mode.
+	 *
+	 * @param	string	$id	Session ID
+	 * @return	bool
+	 */
+	public function validateId($id)
+	{
+		$result = is_file($this->_file_path.$id);
+		clearstatcache(TRUE, $this->_file_path.$id);
+		return $result;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Byte-safe strlen()
+	 *
+	 * @param	string	$str
+	 * @return	int
+	 */
+	protected static function strlen($str)
+	{
+		return (self::$func_overload)
+			? mb_strlen($str, '8bit')
+			: strlen($str);
+	}
+}
diff --git a/system/libraries/Session/drivers/Session_memcached_driver.php b/system/libraries/Session/drivers/Session_memcached_driver.php
new file mode 100644
index 0000000..d140163
--- /dev/null
+++ b/system/libraries/Session/drivers/Session_memcached_driver.php
@@ -0,0 +1,414 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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	https://codeigniter.com/userguide3/libraries/sessions.html
+ */
+class CI_Session_memcached_driver extends CI_Session_driver implements CI_Session_driver_interface {
+
+	/**
+	 * 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 $this->_failure;
+		}
+
+		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 $this->_failure;
+		}
+
+		$this->php5_validate_id();
+
+		return $this->_success;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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 $this->_failure;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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, $this->_lock_key))
+		{
+			return $this->_failure;
+		}
+		// Was the ID regenerated?
+		elseif ($session_id !== $this->_session_id)
+		{
+			if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
+			{
+				return $this->_failure;
+			}
+
+			$this->_fingerprint = md5('');
+			$this->_session_id = $session_id;
+		}
+
+		$key = $this->_key_prefix.$session_id;
+
+		$this->_memcached->replace($this->_lock_key, time(), 300);
+		if ($this->_fingerprint !== ($fingerprint = md5($session_data)))
+		{
+			if ($this->_memcached->set($key, $session_data, $this->_config['expiration']))
+			{
+				$this->_fingerprint = $fingerprint;
+				return $this->_success;
+			}
+
+			return $this->_failure;
+		}
+		elseif (
+			$this->_memcached->touch($key, $this->_config['expiration'])
+			OR ($this->_memcached->getResultCode() === Memcached::RES_NOTFOUND && $this->_memcached->set($key, $session_data, $this->_config['expiration']))
+		)
+		{
+			return $this->_success;
+		}
+
+		return $this->_failure;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Close
+	 *
+	 * Releases locks and closes connection.
+	 *
+	 * @return	bool
+	 */
+	public function close()
+	{
+		if (isset($this->_memcached))
+		{
+			$this->_release_lock();
+			if ( ! $this->_memcached->quit())
+			{
+				return $this->_failure;
+			}
+
+			$this->_memcached = NULL;
+			return $this->_success;
+		}
+
+		return $this->_failure;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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);
+			$this->_cookie_destroy();
+			return $this->_success;
+		}
+
+		return $this->_failure;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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 $this->_success;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update Timestamp
+	 *
+	 * Update session timestamp without modifying data
+	 *
+	 * @param	string	$id	Session ID
+	 * @param	string	$data	Unknown & unused
+	 * @return	bool
+	 */
+	public function updateTimestamp($id, $unknown)
+	{
+		return $this->_memcached->touch($this->_key_prefix.$id, $this->_config['expiration']);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate ID
+	 *
+	 * Checks whether a session ID record exists server-side,
+	 * to enforce session.use_strict_mode.
+	 *
+	 * @param	string	$id	Session ID
+	 * @return	bool
+	 */
+	public function validateId($id)
+	{
+		$this->_memcached->get($this->_key_prefix.$id);
+		return ($this->_memcached->getResultCode() === Memcached::RES_SUCCESS);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get lock
+	 *
+	 * Acquires an (emulated) lock.
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	bool
+	 */
+	protected function _get_lock($session_id)
+	{
+		// PHP 7 reuses the SessionHandler object on regeneration,
+		// so we need to check here if the lock key is for the
+		// correct session ID.
+		if ($this->_lock_key === $this->_key_prefix.$session_id.':lock')
+		{
+			if ( ! $this->_memcached->replace($this->_lock_key, time(), 300))
+			{
+				return ($this->_memcached->getResultCode() === Memcached::RES_NOTFOUND)
+					? $this->_memcached->add($this->_lock_key, time(), 300)
+					: FALSE;
+			}
+
+			return TRUE;
+		}
+
+		// 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;
+			}
+
+			$method = ($this->_memcached->getResultCode() === Memcached::RES_NOTFOUND) ? 'add' : 'set';
+			if ( ! $this->_memcached->$method($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/system/libraries/Session/drivers/Session_redis_driver.php b/system/libraries/Session/drivers/Session_redis_driver.php
new file mode 100644
index 0000000..269dfcd
--- /dev/null
+++ b/system/libraries/Session/drivers/Session_redis_driver.php
@@ -0,0 +1,476 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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	https://codeigniter.com/userguide3/libraries/sessions.html
+ */
+class CI_Session_redis_driver extends CI_Session_driver implements CI_Session_driver_interface {
+
+	/**
+	 * phpRedis instance
+	 *
+	 * @var	Redis
+	 */
+	protected $_redis;
+
+	/**
+	 * Key prefix
+	 *
+	 * @var	string
+	 */
+	protected $_key_prefix = 'ci_session:';
+
+	/**
+	 * Lock key
+	 *
+	 * @var	string
+	 */
+	protected $_lock_key;
+
+	/**
+	 * Key exists flag
+	 *
+	 * @var bool
+	 */
+	protected $_key_exists = FALSE;
+
+	/**
+	 * Name of setTimeout() method in phpRedis
+	 *
+	 * Due to some deprecated methods in phpRedis, we need to call the
+	 * specific methods depending on the version of phpRedis.
+	 *
+	 * @var string
+	 */
+	protected $_setTimeout_name;
+
+	/**
+	 * Name of delete() method in phpRedis
+	 *
+	 * Due to some deprecated methods in phpRedis, we need to call the
+	 * specific methods depending on the version of phpRedis.
+	 *
+	 * @var string
+	 */
+	protected $_delete_name;
+
+	/**
+	 * Success return value of ping() method in phpRedis
+	 *
+	 * @var mixed
+	 */
+	protected $_ping_success;
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Class constructor
+	 *
+	 * @param	array	$params	Configuration parameters
+	 * @return	void
+	 */
+	public function __construct(&$params)
+	{
+		parent::__construct($params);
+
+		// Detect the names of some methods in phpRedis instance
+		if (version_compare(phpversion('redis'), '5', '>='))
+		{
+			$this->_setTimeout_name = 'expire';
+			$this->_delete_name = 'del';
+			$this->_ping_success = TRUE;
+		}
+		else
+		{
+			$this->_setTimeout_name = 'setTimeout';
+			$this->_delete_name = 'delete';
+			$this->_ping_success = '+PONG';
+		}
+
+		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 $this->_failure;
+		}
+
+		$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;
+			$this->php5_validate_id();
+			return $this->_success;
+		}
+
+		return $this->_failure;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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 = $this->_redis->get($this->_key_prefix.$session_id);
+
+			is_string($session_data)
+				? $this->_key_exists = TRUE
+				: $session_data = '';
+
+			$this->_fingerprint = md5($session_data);
+			return $session_data;
+		}
+
+		return $this->_failure;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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, $this->_lock_key))
+		{
+			return $this->_failure;
+		}
+		// Was the ID regenerated?
+		elseif ($session_id !== $this->_session_id)
+		{
+			if ( ! $this->_release_lock() OR ! $this->_get_lock($session_id))
+			{
+				return $this->_failure;
+			}
+
+			$this->_key_exists = FALSE;
+			$this->_session_id = $session_id;
+		}
+
+		$this->_redis->{$this->_setTimeout_name}($this->_lock_key, 300);
+		if ($this->_fingerprint !== ($fingerprint = md5($session_data)) OR $this->_key_exists === FALSE)
+		{
+			if ($this->_redis->set($this->_key_prefix.$session_id, $session_data, $this->_config['expiration']))
+			{
+				$this->_fingerprint = $fingerprint;
+				$this->_key_exists = TRUE;
+				return $this->_success;
+			}
+
+			return $this->_failure;
+		}
+
+		return ($this->_redis->{$this->_setTimeout_name}($this->_key_prefix.$session_id, $this->_config['expiration']))
+			? $this->_success
+			: $this->_failure;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Close
+	 *
+	 * Releases locks and closes connection.
+	 *
+	 * @return	bool
+	 */
+	public function close()
+	{
+		if (isset($this->_redis))
+		{
+			try {
+				if ($this->_redis->ping() === $this->_ping_success)
+				{
+					$this->_release_lock();
+					if ($this->_redis->close() === FALSE)
+					{
+						return $this->_failure;
+					}
+				}
+			}
+			catch (RedisException $e)
+			{
+				log_message('error', 'Session: Got RedisException on close(): '.$e->getMessage());
+			}
+
+			$this->_redis = NULL;
+			return $this->_success;
+		}
+
+		return $this->_success;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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->{$this->_delete_name}($this->_key_prefix.$session_id)) !== 1)
+			{
+				log_message('debug', 'Session: Redis::'.$this->_delete_name.'() expected to return 1, got '.var_export($result, TRUE).' instead.');
+			}
+
+			$this->_cookie_destroy();
+			return $this->_success;
+		}
+
+		return $this->_failure;
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * 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 $this->_success;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Update Timestamp
+	 *
+	 * Update session timestamp without modifying data
+	 *
+	 * @param	string	$id	Session ID
+	 * @param	string	$data	Unknown & unused
+	 * @return	bool
+	 */
+	public function updateTimestamp($id, $unknown)
+	{
+		return $this->_redis->{$this->_setTimeout_name}($this->_key_prefix.$id, $this->_config['expiration']);
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Validate ID
+	 *
+	 * Checks whether a session ID record exists server-side,
+	 * to enforce session.use_strict_mode.
+	 *
+	 * @param	string	$id	Session ID
+	 * @return	bool
+	 */
+	public function validateId($id)
+	{
+		return (bool) $this->_redis->exists($this->_key_prefix.$id);
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * Get lock
+	 *
+	 * Acquires an (emulated) lock.
+	 *
+	 * @param	string	$session_id	Session ID
+	 * @return	bool
+	 */
+	protected function _get_lock($session_id)
+	{
+		// PHP 7 reuses the SessionHandler object on regeneration,
+		// so we need to check here if the lock key is for the
+		// correct session ID.
+		if ($this->_lock_key === $this->_key_prefix.$session_id.':lock')
+		{
+			return $this->_redis->{$this->_setTimeout_name}($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 ($ttl === -2 && ! $this->_redis->set($lock_key, time(), array('nx', 'ex' => 300)))
+			{
+				// Sleep for 1s to wait for lock releases.
+				sleep(1);
+				continue;
+			}
+			elseif ( ! $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->{$this->_delete_name}($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/system/libraries/Session/drivers/index.html b/system/libraries/Session/drivers/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/libraries/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/system/libraries/Session/index.html b/system/libraries/Session/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/libraries/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/system/libraries/Table.php b/system/libraries/Table.php
new file mode 100644
index 0000000..35f456a
--- /dev/null
+++ b/system/libraries/Table.php
@@ -0,0 +1,539 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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;
+		return $this;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * 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	$object	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/system/libraries/Trackback.php b/system/libraries/Trackback.php
new file mode 100644
index 0000000..9246ec6
--- /dev/null
+++ b/system/libraries/Trackback.php
@@ -0,0 +1,557 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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 (stripos($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/system/libraries/Typography.php b/system/libraries/Typography.php
new file mode 100644
index 0000000..108bc77
--- /dev/null
+++ b/system/libraries/Typography.php
@@ -0,0 +1,425 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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 preceding 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/system/libraries/Unit_test.php b/system/libraries/Unit_test.php
new file mode 100644
index 0000000..e1b94f0
--- /dev/null
+++ b/system/libraries/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) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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))
+		{
+			$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', '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/system/libraries/Upload.php b/system/libraries/Upload.php
new file mode 100644
index 0000000..434b6b1
--- /dev/null
+++ b/system/libraries/Upload.php
@@ -0,0 +1,1327 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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	$config
+	 * @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);
+		}
+
+		if ($this->file_ext_tolower && ($ext_length = strlen($this->file_ext)))
+		{
+			// file_ext was previously lower-cased by a get_extension() call
+			$this->file_name = substr($this->file_name, 0, -$ext_length).$this->file_ext;
+		}
+
+		/*
+		 * 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'		=> substr($this->file_name, 0, -strlen($this->file_ext)),
+				'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;
+		}
+
+		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', 'image/webp');
+
+		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', 'webp'), 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')) > 0)
+		{
+			$memory_limit = str_split($memory_limit, strspn($memory_limit, '1234567890'));
+			if ( ! empty($memory_limit[1]))
+			{
+				switch ($memory_limit[1][0])
+				{
+					case 'g':
+					case 'G':
+						$memory_limit[0] *= 1024 * 1024 * 1024;
+						break;
+					case 'm':
+					case 'M':
+						$memory_limit[0] *= 1024 * 1024;
+						break;
+					default:
+						break;
+				}
+			}
+
+			$memory_limit = (int) 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
+		 *
+		 * Apparently XAMPP, CentOS, cPanel and who knows what
+		 * other PHP distribution channels EXPLICITLY DISABLE
+		 * ext/fileinfo, which is otherwise enabled by default
+		 * since PHP 5.3 ...
+		 */
+		if (function_exists('finfo_file'))
+		{
+			$finfo = @finfo_open(FILEINFO_MIME);
+			if ($finfo !== FALSE) // 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 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/system/libraries/User_agent.php b/system/libraries/User_agent.php
new file mode 100644
index 0000000..6dfabda
--- /dev/null
+++ b/system/libraries/User_agent.php
@@ -0,0 +1,682 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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()
+	{
+		$this->_load_agent_file();
+
+		if (isset($_SERVER['HTTP_USER_AGENT']))
+		{
+			$this->agent = trim($_SERVER['HTTP_USER_AGENT']);
+			$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((string) 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/system/libraries/Xmlrpc.php b/system/libraries/Xmlrpc.php
new file mode 100644
index 0000000..a22841c
--- /dev/null
+++ b/system/libraries/Xmlrpc.php
@@ -0,0 +1,1921 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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 Response
+	 *
+	 * @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 (stripos($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'))
+				{
+					foreach (array_keys($value[0]) as $k)
+					{
+						$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		https://codeigniter.com/userguide3/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;
+
+		stream_set_timeout($fp, $this->timeout); // set timeout for subsequent operations
+
+		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;
+				}
+			}
+			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		https://codeigniter.com/userguide3/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))
+		{
+			foreach ($array as $key => &$value)
+			{
+				if (is_array($value))
+				{
+					$array[$key] = $this->decode($value);
+				}
+				elseif ($this->xss_clean)
+				{
+					$array[$key] = $CI->security->xss_clean($value);
+				}
+			}
+
+			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();
+
+			foreach ($xmlrpc_val->me['struct'] as $key => &$value)
+			{
+				$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		https://codeigniter.com/userguide3/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, TRUE))
+		{
+			$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))
+		{
+			foreach ($array as $key => &$value)
+			{
+				if (is_array($value))
+				{
+					$array[$key] = $this->output_parameters($value);
+				}
+				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($value);
+				}
+			}
+
+			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();
+
+			foreach ($param->me['struct'] as $key => &$value)
+			{
+				$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		https://codeigniter.com/userguide3/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);
+				foreach ($val as $key2 => &$val2)
+				{
+					$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)
+	{
+		$array = $o->me;
+		list($value, $type) = array(reset($array), key($array));
+		return "<value>\n".$this->serializedata($type, $value)."</value>\n";
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Scalar value
+	 *
+	 * @return	mixed
+	 */
+	public function scalarval()
+	{
+		return reset($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) ? date('Ymd\TH:i:s', $time) : gmdate('Ymd\TH:i:s', $time);
+	}
+
+} // END XML_RPC_Values Class
diff --git a/system/libraries/Xmlrpcs.php b/system/libraries/Xmlrpcs.php
new file mode 100644
index 0000000..b91d3fc
--- /dev/null
+++ b/system/libraries/Xmlrpcs.php
@@ -0,0 +1,620 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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   = ! empty($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 && ( ! method_exists($method_parts[0], $method_parts[1]) OR ! (new ReflectionMethod($method_parts[0], $method_parts[1]))->isPublic()))
+			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);
+			}
+
+			return $this->object->{$method_parts[1]}($m);
+		}
+
+		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'));
+		}
+
+		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_value, $scalar_type) = array(reset($methName->me), key($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($b, $a) = array(reset($params->me), key($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/system/libraries/Zip.php b/system/libraries/Zip.php
new file mode 100644
index 0000000..6b50819
--- /dev/null
+++ b/system/libraries/Zip.php
@@ -0,0 +1,534 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP
+ *
+ * This content is released under the MIT License (MIT)
+ *
+ * Copyright (c) 2019 - 2022, CodeIgniter Foundation
+ *
+ * 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. (https://ellislab.com/)
+ * @copyright	Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/)
+ * @copyright	Copyright (c) 2019 - 2022, CodeIgniter Foundation (https://codeigniter.com/)
+ * @license	https://opensource.org/licenses/MIT	MIT License
+ * @link	https://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		https://codeigniter.com/userguide3/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;
+
+	/**
+	 * mbstring.func_overload flag
+	 *
+	 * @var	bool
+	 */
+	protected static $func_overload;
+
+	/**
+	 * Initialize zip compression class
+	 *
+	 * @return	void
+	 */
+	public function __construct()
+	{
+		isset(self::$func_overload) OR self::$func_overload = ( ! is_php('8.0') && extension_loaded('mbstring') && @ini_get('mbstring.func_overload'));
+
+		$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', self::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', self::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 = self::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 = self::strlen($data);
+		$crc32  = crc32($data);
+		$gzdata = self::substr(gzcompress($data, $this->compression_level), 2, -4);
+		$compressed_size = self::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', self::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', self::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 = self::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;
+		}
+
+		// @see https://github.com/bcit-ci/CodeIgniter/issues/5864
+		$footer = $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', self::strlen($this->directory)) // size of central dir
+			.pack('V', self::strlen($this->zipdata)) // offset to start of central dir
+			."\x00\x00"; // .zip file comment length
+		return $this->zipdata.$footer;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * 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 = self::strlen($data); $written < $length; $written += $result)
+		{
+			if (($result = fwrite($fp, self::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;
+	}
+
+	// --------------------------------------------------------------------
+
+	/**
+	 * Byte-safe strlen()
+	 *
+	 * @param	string	$str
+	 * @return	int
+	 */
+	protected static function strlen($str)
+	{
+		return (self::$func_overload)
+			? 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_overload)
+		{
+			// 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/system/libraries/index.html b/system/libraries/index.html
new file mode 100644
index 0000000..b702fbc
--- /dev/null
+++ b/system/libraries/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<title>403 Forbidden</title>
+</head>
+<body>
+
+<p>Directory access is forbidden.</p>
+
+</body>
+</html>