From 6f1342f25b73d4e84c44abed8da2271a3e82e939 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Thu, 8 Jun 2023 06:28:59 +0100 Subject: [PATCH 01/68] Fix PHP 8.1 E_DEPRECATED issue when the page has no output. Signed-off-by: Bruno Moreira --- system/core/Output.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/core/Output.php b/system/core/Output.php index 02f3933f515..2109774932c 100644 --- a/system/core/Output.php +++ b/system/core/Output.php @@ -456,7 +456,7 @@ public function _display($output = NULL) $elapsed = $BM->elapsed_time('total_execution_time_start', 'total_execution_time_end'); - if ($this->parse_exec_vars === TRUE) + if ($this->parse_exec_vars === TRUE && !empty($output)) { $memory = round(memory_get_usage() / 1024 / 1024, 2).'MB'; $output = str_replace(array('{elapsed_time}', '{memory_usage}'), array($elapsed, $memory), $output); From 32dcc1d14f87ee5a69e4f59974b9409cff4ff4a2 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Fri, 9 Jun 2023 06:18:44 +0100 Subject: [PATCH 02/68] Make the PDO driver always throw exceptions on failed queries. --- system/database/drivers/pdo/pdo_driver.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/system/database/drivers/pdo/pdo_driver.php b/system/database/drivers/pdo/pdo_driver.php index 559e865552c..0736f837d28 100644 --- a/system/database/drivers/pdo/pdo_driver.php +++ b/system/database/drivers/pdo/pdo_driver.php @@ -65,7 +65,9 @@ class CI_DB_pdo_driver extends CI_DB { * * @var array */ - public $options = array(); + public $options = array( + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ); // -------------------------------------------------------------------- From 72786e4a0c5c237c0b9cb7974cf02941eec51ac6 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Fri, 9 Jun 2023 06:19:06 +0100 Subject: [PATCH 03/68] Make query builder variables accessible to end-user code. --- system/database/DB_query_builder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/database/DB_query_builder.php b/system/database/DB_query_builder.php index de6aa04fc08..c8592352818 100644 --- a/system/database/DB_query_builder.php +++ b/system/database/DB_query_builder.php @@ -71,7 +71,7 @@ abstract class CI_DB_query_builder extends CI_DB_driver { * * @var array */ - protected $qb_select = array(); + public $qb_select = array(); /** * QB DISTINCT flag @@ -141,7 +141,7 @@ abstract class CI_DB_query_builder extends CI_DB_driver { * * @var array */ - protected $qb_orderby = array(); + public $qb_orderby = array(); /** * QB data sets From 55d7657ba606e3519461a65d334c07371d36d38d Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Tue, 13 Jun 2023 07:04:07 +0100 Subject: [PATCH 04/68] Add debugbar code back. --- system/core/Loader.php | 15 ++++++++++++++- system/database/drivers/pdo/pdo_driver.php | 13 ++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/system/core/Loader.php b/system/core/Loader.php index 648b7cfc7c2..90df8f9523a 100644 --- a/system/core/Loader.php +++ b/system/core/Loader.php @@ -498,7 +498,20 @@ class_exists('CI_DB', FALSE) OR $this->database(); */ 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)); + $measure = "View - $view"; + $CI =& get_instance(); + + if ($CI->debugbar) { + $CI->debugbar["time"]->startMeasure($measure); + } + + $result = $this->_ci_load(array('_ci_view' => $view, '_ci_vars' => $this->_ci_prepare_view_vars($vars), '_ci_return' => $return)); + + if ($CI->debugbar) { + $CI->debugbar["time"]->stopMeasure($measure); + } + + return $result; } // -------------------------------------------------------------------- diff --git a/system/database/drivers/pdo/pdo_driver.php b/system/database/drivers/pdo/pdo_driver.php index 0736f837d28..a806b4427d8 100644 --- a/system/database/drivers/pdo/pdo_driver.php +++ b/system/database/drivers/pdo/pdo_driver.php @@ -36,6 +36,9 @@ * @since Version 2.1.0 * @filesource */ + +use DebugBar\DataCollector\PDO\TraceablePDO; + defined('BASEPATH') OR exit('No direct script access allowed'); /** @@ -129,6 +132,8 @@ public function __construct($params) */ public function db_connect($persistent = FALSE) { + global $debugbar; + if ($persistent === TRUE) { $this->options[PDO::ATTR_PERSISTENT] = TRUE; @@ -144,7 +149,13 @@ public function db_connect($persistent = FALSE) try { - return new PDO($this->dsn, $this->username, $this->password, $this->options); + if ($debugbar) { + $pdo = new TraceablePDO(new PDO($this->dsn, $this->username, $this->password, $this->options)); + $debugbar->addCollector(new DebugBar\DataCollector\PDO\PDOCollector($pdo)); + return $pdo; + } else { + return new PDO($this->dsn, $this->username, $this->password, $this->options); + } } catch (PDOException $e) { From dedd7a63a5bfa3af7711c8be5f01e1369758bb58 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Tue, 13 Jun 2023 07:09:25 +0100 Subject: [PATCH 05/68] Fix issue with previous commit. --- system/core/Loader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/core/Loader.php b/system/core/Loader.php index 90df8f9523a..138d66b7e6e 100644 --- a/system/core/Loader.php +++ b/system/core/Loader.php @@ -501,13 +501,13 @@ public function view($view, $vars = array(), $return = FALSE) $measure = "View - $view"; $CI =& get_instance(); - if ($CI->debugbar) { + if (property_exists($CI, "debugbar") && $CI->debugbar) { $CI->debugbar["time"]->startMeasure($measure); } $result = $this->_ci_load(array('_ci_view' => $view, '_ci_vars' => $this->_ci_prepare_view_vars($vars), '_ci_return' => $return)); - if ($CI->debugbar) { + if (property_exists($CI, "debugbar") && $CI->debugbar && $CI->debugbar["time"]->hasStartedMeasure($measure)) { $CI->debugbar["time"]->stopMeasure($measure); } From 6bff5887f280a35a629d8ec47d2d636479088429 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Mon, 19 Jun 2023 06:43:30 +0100 Subject: [PATCH 06/68] Merge PHP 8.2 support from gxgpet * Adding PHP 8.2 support * Adds $uri prop in Router, and AllowDynamicProps attribute for CI_Controller * Adding missing `dest_image` property in CI_Image_lib --------- Co-authored-by: George Petculescu Co-authored-by: George Petculescu --- .github/workflows/test-phpunit.yml | 20 +++++++++++++++++++- system/core/Controller.php | 1 + system/core/Loader.php | 1 + system/core/Router.php | 7 +++++++ system/core/URI.php | 7 +++++++ system/database/DB_driver.php | 1 + system/libraries/Driver.php | 1 + system/libraries/Image_lib.php | 8 ++++++++ system/libraries/Table.php | 4 ++-- tests/codeigniter/core/Loader_test.php | 2 +- tests/codeigniter/libraries/Upload_test.php | 7 ++++--- tests/mocks/ci_testcase.php | 1 + 12 files changed, 53 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml index c660b937e04..d965765bce7 100644 --- a/.github/workflows/test-phpunit.yml +++ b/.github/workflows/test-phpunit.yml @@ -15,10 +15,28 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.1', '8.0', '7.4', '7.3', '7.2', '7.1', '7.0', '5.6', '5.5', '5.4'] + php: [ '8.2', '8.1', '8.0', '7.4', '7.3', '7.2', '7.1', '7.0', '5.6', '5.5', '5.4'] DB: [ 'pdo/mysql', 'pdo/pgsql', 'pdo/sqlite', 'mysqli', 'pgsql', 'sqlite' ] compiler: [ default ] include: + - php: '8.2' + DB: 'pdo/mysql' + compiler: jit + - php: '8.2' + DB: 'pdo/pgsql' + compiler: jit + - php: '8.2' + DB: 'pdo/sqlite' + compiler: jit + - php: '8.2' + DB: 'mysqli' + compiler: jit + - php: '8.2' + DB: 'pgsql' + compiler: jit + - php: '8.2' + DB: 'sqlite' + compiler: jit - php: '8.1' DB: 'pdo/mysql' compiler: jit diff --git a/system/core/Controller.php b/system/core/Controller.php index aeccd60ee4c..3713ae06308 100644 --- a/system/core/Controller.php +++ b/system/core/Controller.php @@ -50,6 +50,7 @@ * @author EllisLab Dev Team * @link https://codeigniter.com/userguide3/general/controllers.html */ +#[AllowDynamicProperties] class CI_Controller { /** diff --git a/system/core/Loader.php b/system/core/Loader.php index 138d66b7e6e..b7380c05098 100644 --- a/system/core/Loader.php +++ b/system/core/Loader.php @@ -49,6 +49,7 @@ * @author EllisLab Dev Team * @link https://codeigniter.com/userguide3/libraries/loader.html */ +#[AllowDynamicProperties] class CI_Loader { // All these are set automatically. Don't mess with them. diff --git a/system/core/Router.php b/system/core/Router.php index e0fb922f1a6..8302dc06006 100644 --- a/system/core/Router.php +++ b/system/core/Router.php @@ -58,6 +58,13 @@ class CI_Router { */ public $config; + /** + * CI_URI class object + * + * @var object + */ + public $uri; + /** * List of routes * diff --git a/system/core/URI.php b/system/core/URI.php index 1e948588354..95b9c926c96 100644 --- a/system/core/URI.php +++ b/system/core/URI.php @@ -51,6 +51,13 @@ */ class CI_URI { + /** + * CI_Config instance + * + * @var CI_Config + */ + public $config; + /** * List of cached URI segments * diff --git a/system/database/DB_driver.php b/system/database/DB_driver.php index de03a418567..6cbd7026e91 100644 --- a/system/database/DB_driver.php +++ b/system/database/DB_driver.php @@ -51,6 +51,7 @@ * @author EllisLab Dev Team * @link https://codeigniter.com/userguide3/database/ */ +#[AllowDynamicProperties] abstract class CI_DB_driver { /** diff --git a/system/libraries/Driver.php b/system/libraries/Driver.php index 84f0b6c3e76..23f4d23f3d0 100644 --- a/system/libraries/Driver.php +++ b/system/libraries/Driver.php @@ -50,6 +50,7 @@ * @author EllisLab Dev Team * @link */ +#[AllowDynamicProperties] class CI_Driver_Library { /** diff --git a/system/libraries/Image_lib.php b/system/libraries/Image_lib.php index 4e5fc7be6fc..9067eff692f 100644 --- a/system/libraries/Image_lib.php +++ b/system/libraries/Image_lib.php @@ -85,6 +85,14 @@ class CI_Image_lib { */ public $new_image = ''; + + /** + * Path to destination image + * + * @var string + */ + public $dest_image = ''; + /** * Image width * diff --git a/system/libraries/Table.php b/system/libraries/Table.php index a033ced212c..60b9fddcf84 100644 --- a/system/libraries/Table.php +++ b/system/libraries/Table.php @@ -490,12 +490,12 @@ protected function _compile_template() return; } - $this->temp = $this->_default_template(); + $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]; + $this->template[$val] = $temp[$val]; } } } diff --git a/tests/codeigniter/core/Loader_test.php b/tests/codeigniter/core/Loader_test.php index df9c9f44b68..b14b7178019 100644 --- a/tests/codeigniter/core/Loader_test.php +++ b/tests/codeigniter/core/Loader_test.php @@ -125,7 +125,7 @@ public function test_library_config() // Create library in VFS $lib = 'unit_test_config_lib'; $class = 'CI_'.ucfirst($lib); - $content = 'config = $params; } }'; + $content = "config = $params; } }'; $this->ci_vfs_create(ucfirst($lib), $content, $this->ci_base_root, 'libraries'); // Create config file diff --git a/tests/codeigniter/libraries/Upload_test.php b/tests/codeigniter/libraries/Upload_test.php index 74a7d2c22df..b660d0ec85f 100644 --- a/tests/codeigniter/libraries/Upload_test.php +++ b/tests/codeigniter/libraries/Upload_test.php @@ -59,9 +59,6 @@ function test_data() $data = array( 'file_name' => 'hello.txt', 'file_type' => 'text/plain', - 'file_path' => '/tmp/', - 'full_path' => '/tmp/hello.txt', - 'raw_name' => 'hello', 'orig_name' => 'hello.txt', 'client_name' => '', 'file_ext' => '.txt', @@ -80,6 +77,10 @@ function test_data() $this->upload->{$k} = $v; } + $data['file_path'] = '/tmp/'; + $data['full_path'] = '/tmp/hello.txt'; + $data['raw_name'] = 'hello'; + $this->assertEquals('hello.txt', $this->upload->data('file_name')); $this->assertEquals($data, $this->upload->data()); } diff --git a/tests/mocks/ci_testcase.php b/tests/mocks/ci_testcase.php index 3ebb6b822e9..e755bd0be5b 100644 --- a/tests/mocks/ci_testcase.php +++ b/tests/mocks/ci_testcase.php @@ -1,5 +1,6 @@ Date: Fri, 3 Nov 2023 18:48:27 +0000 Subject: [PATCH 07/68] Update package name for release in Packagist. # Conflicts: # readme.rst --- composer.json | 9 ++---- readme.rst | 79 +++++++++------------------------------------------ 2 files changed, 17 insertions(+), 71 deletions(-) diff --git a/composer.json b/composer.json index f5b4e8db43d..e7360a93de7 100644 --- a/composer.json +++ b/composer.json @@ -1,14 +1,11 @@ { "description": "The CodeIgniter framework", - "name": "codeigniter/framework", + "name": "pocketarc/codeigniter", "type": "project", - "homepage": "https://codeigniter.com", + "homepage": "https://github.com/pocketarc/codeigniter", "license": "MIT", "support": { - "forum": "https://forum.codeigniter.com/", - "wiki": "https://github.com/bcit-ci/CodeIgniter/wiki", - "slack": "https://codeigniterchat.slack.com", - "source": "https://github.com/bcit-ci/CodeIgniter" + "source": "https://github.com/pocketarc/codeigniter" }, "require": { "php": ">=5.4.8" diff --git a/readme.rst b/readme.rst index 424848ac3e5..ce7e65c94e0 100644 --- a/readme.rst +++ b/readme.rst @@ -1,82 +1,31 @@ -################### -What is CodeIgniter -################### +######################## +What is this repository? +######################## -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. +This is a fork of CodeIgniter 3, with the goal of keeping it up to date with PHP 8.2 and beyond. There is no intention to add new features or change the way CI3 works. This is purely a maintenance fork. -************* -CodeIgniter 3 -************* +The original CodeIgniter 3.x branch is no longer maintained, and has not been updated to work with PHP 8.2 or newer. This fork is intended to fill that gap. -This repository is for the legacy version, CodeIgniter 3. -`CodeIgniter 4 `_ is the latest -version of the framework. +If the original CodeIgniter 3.x branch is updated to work with PHP 8.2 or newer, and starts to be maintained again, this fork might be retired. -CodeIgniter 3 is the legacy version of the framework, intended for use with PHP -5.6+. This version is in maintenance, receiving mostly just security updates. +**************** +Issues and Pulls +**************** -******************* -Release Information -******************* - -This repo contains in-development code for future releases. To download the -latest stable release please visit the `CodeIgniter Downloads -`_ page. - -************************** -Changelog and New Features -************************** - -You can find a list of all changes for each release in the `user -guide change log `_. +Issues and Pull Requests are welcome, but please note that this is a maintenance fork. New features will not be accepted. If you have a new feature you would like to see in CodeIgniter, please submit it to the original CodeIgniter 3.x branch. ******************* Server Requirements ******************* -PHP version 5.6 or newer is recommended. - -It should work on 5.4.8 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. +PHP version 5.4 or newer, same as the original CI3 requirements. ************ Installation ************ -Please see the `installation section `_ -of the CodeIgniter User Guide. - -******* -License -******* - -Please see the `license -agreement `_. - -********* -Resources -********* - -- `User Guide `_ -- `Contributing Guide `_ -- `Language File Translations `_ -- `Community Forums `_ -- `Community Wiki `_ -- `Community Slack Channel `_ - -Report security issues to our `Security Panel `_ -or via our `page on HackerOne `_, thank you. +Simple: Just use composer. -*************** -Acknowledgement -*************** +.. code-block:: bash -The CodeIgniter team would like to thank EllisLab, all the -contributors to the CodeIgniter project and you, the CodeIgniter user. + composer require pocketarc/codeigniter From 7b34f8bec85c9f1de1debe456a4cbf83e18319e2 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sun, 25 Feb 2024 06:36:20 +0000 Subject: [PATCH 08/68] Fixes backward incompatibility caused by the upgrade from CI 3.1 to 3.2-dev. --- system/core/Model.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/system/core/Model.php b/system/core/Model.php index 1ba10fbb79d..b2bbbd4d484 100644 --- a/system/core/Model.php +++ b/system/core/Model.php @@ -49,6 +49,14 @@ */ class CI_Model { + /** + * Class constructor + * + * @link https://github.com/bcit-ci/CodeIgniter/issues/5332 + * @return void + */ + public function __construct() {} + /** * __get magic * From cec56c54f31623e2afc598abde42c0dbbeb839f1 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sun, 25 Feb 2024 06:44:18 +0000 Subject: [PATCH 09/68] Fix PHP 8.x compatibility (#5) Co-authored-by: Roger --- system/helpers/text_helper.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/helpers/text_helper.php b/system/helpers/text_helper.php index 506d45aca70..979e1458044 100644 --- a/system/helpers/text_helper.php +++ b/system/helpers/text_helper.php @@ -423,6 +423,8 @@ function convert_accented_characters($str) $array_from = array_keys($foreign_characters); $array_to = array_values($foreign_characters); } + + $str = (gettype($str) === 'NULL') ? '' : $str; return preg_replace($array_from, $array_to, $str); } From ad09d76759f0ab4c8b553f304790a99456d3bf84 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sun, 25 Feb 2024 06:56:02 +0000 Subject: [PATCH 10/68] Fixes backward incompatibility caused by the upgrade from CI 3.1 to 3.2-dev. --- readme.rst | 6 ++--- system/core/Router.php | 43 ++++++++++++++++++++++++++++++++++ system/helpers/file_helper.php | 19 +++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/readme.rst b/readme.rst index ce7e65c94e0..ddcfe6c6843 100644 --- a/readme.rst +++ b/readme.rst @@ -2,11 +2,11 @@ What is this repository? ######################## -This is a fork of CodeIgniter 3, with the goal of keeping it up to date with PHP 8.2 and beyond. There is no intention to add new features or change the way CI3 works. This is purely a maintenance fork. +This is a fork of CodeIgniter 3, with the goal of keeping it up to date with PHP 8.3 and beyond. There is no intention to add new features or change the way CI3 works. This is purely a maintenance fork. -The original CodeIgniter 3.x branch is no longer maintained, and has not been updated to work with PHP 8.2 or newer. This fork is intended to fill that gap. +The original CodeIgniter 3.x branch is no longer maintained, and has not been updated to work with PHP 8.2 (or 8.3). This fork is intended to fill that gap. -If the original CodeIgniter 3.x branch is updated to work with PHP 8.2 or newer, and starts to be maintained again, this fork might be retired. +If the original CodeIgniter 3.x branch is updated to work with PHP 8.3 or newer, and starts to be maintained again, this fork might be retired. **************** Issues and Pulls diff --git a/system/core/Router.php b/system/core/Router.php index 8302dc06006..6dca57cf3d2 100644 --- a/system/core/Router.php +++ b/system/core/Router.php @@ -446,6 +446,19 @@ public function set_class($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 * @@ -459,6 +472,19 @@ public function set_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 * @@ -477,4 +503,21 @@ public function set_directory($dir, $append = FALSE) $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/helpers/file_helper.php b/system/helpers/file_helper.php index a751f771eb9..dc5ee6b5a5c 100644 --- a/system/helpers/file_helper.php +++ b/system/helpers/file_helper.php @@ -50,6 +50,25 @@ // ------------------------------------------------------------------------ +if ( ! function_exists('read_file')) +{ + /** + * Read File + * + * Opens the file specified in the path and returns it as a string. + * + * @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')) { /** From 25a96c4b61116bb22e445588146570ac6ef751f9 Mon Sep 17 00:00:00 2001 From: AJ Dunn Date: Mon, 28 Oct 2024 02:41:40 +1030 Subject: [PATCH 11/68] Fix deprecated dynamic property creation in CI_Profiler Library. Due to PHP 8.2 deprecation of dynamic property creation, we can explicitly declare all _compile_* properties in the CI_Profiler class. These properties are known as they are created based on the $_available_sections array. This would be a clearer approach than just using #[AllowDynamicProperties] --- system/libraries/Profiler.php | 70 +++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/system/libraries/Profiler.php b/system/libraries/Profiler.php index d423c14811c..4c3537bd25b 100644 --- a/system/libraries/Profiler.php +++ b/system/libraries/Profiler.php @@ -73,6 +73,76 @@ class CI_Profiler { 'config' ); + /** + * Compile benchmarks + * + * @var bool + */ + protected $_compile_benchmarks; + + /** + * Compile GET data + * + * @var bool + */ + protected $_compile_get; + + /** + * Compile memory usage + * + * @var bool + */ + protected $_compile_memory_usage; + + /** + * Compile POST data + * + * @var bool + */ + protected $_compile_post; + + /** + * Compile URI string + * + * @var bool + */ + protected $_compile_uri_string; + + /** + * Compile controller info + * + * @var bool + */ + protected $_compile_controller_info; + + /** + * Compile queries + * + * @var bool + */ + protected $_compile_queries; + + /** + * Compile HTTP headers + * + * @var bool + */ + protected $_compile_http_headers; + + /** + * Compile session data + * + * @var bool + */ + protected $_compile_session_data; + + /** + * Compile config data + * + * @var bool + */ + protected $_compile_config; + /** * Number of queries to show before making the additional queries togglable * From ab8b010a1e65e66869162b0a6993bfe120ac484a Mon Sep 17 00:00:00 2001 From: siemova <5099527+siemova@users.noreply.github.com> Date: Sun, 27 Oct 2024 11:21:46 -0500 Subject: [PATCH 12/68] Fix warning and ignored TTL in Redis cache driver. --- system/libraries/Cache/drivers/Cache_redis.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/libraries/Cache/drivers/Cache_redis.php b/system/libraries/Cache/drivers/Cache_redis.php index 19fbdc0cae4..27454329e81 100644 --- a/system/libraries/Cache/drivers/Cache_redis.php +++ b/system/libraries/Cache/drivers/Cache_redis.php @@ -163,7 +163,7 @@ public function get($key) { $data = $this->_redis->hMGet($key, array('__ci_type', '__ci_value')); - if ($value !== FALSE && $this->_redis->sIsMember('_ci_redis_serialized', $key)) + if ($data === FALSE || $this->_redis->sIsMember('_ci_redis_serialized', $key)) { return FALSE; } @@ -223,6 +223,7 @@ public function save($id, $data, $ttl = 60, $raw = FALSE) } else { + $this->_redis->expireAt($id, time() + $ttl); $this->_redis->{static::$_sRemove_name}('_ci_redis_serialized', $id); } From 96fcced12923f17e9666774075a1811e14271015 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Tue, 29 Oct 2024 06:12:23 -0600 Subject: [PATCH 13/68] Adds compatibility with PHP 8.4. --- index.php | 6 +++++- readme.rst | 6 +++--- system/core/Exceptions.php | 6 +++++- tests/Bootstrap.php | 9 +++++++-- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/index.php b/index.php index 8b095a77a78..81a588c22f5 100644 --- a/index.php +++ b/index.php @@ -73,7 +73,11 @@ case 'testing': case 'production': ini_set('display_errors', 0); - error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED); + if (version_compare(PHP_VERSION, '8.4', '>=')) { + error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_USER_NOTICE & ~E_USER_DEPRECATED); + } else { + error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED); + } break; default: diff --git a/readme.rst b/readme.rst index ddcfe6c6843..e0100b8f32e 100644 --- a/readme.rst +++ b/readme.rst @@ -2,11 +2,11 @@ What is this repository? ######################## -This is a fork of CodeIgniter 3, with the goal of keeping it up to date with PHP 8.3 and beyond. There is no intention to add new features or change the way CI3 works. This is purely a maintenance fork. +This is a fork of CodeIgniter 3, with the goal of keeping it up to date with **PHP 8.4** and beyond. There is no intention to add new features or change the way CI3 works. This is purely a maintenance fork. -The original CodeIgniter 3.x branch is no longer maintained, and has not been updated to work with PHP 8.2 (or 8.3). This fork is intended to fill that gap. +The original CodeIgniter 3.x branch is no longer maintained, and has not been updated to work with PHP 8.2, or any newer version. This fork is intended to fill that gap. -If the original CodeIgniter 3.x branch is updated to work with PHP 8.3 or newer, and starts to be maintained again, this fork might be retired. +If the original CodeIgniter 3.x branch is updated to work with PHP 8.2+, and starts to be maintained again, this fork might be retired. **************** Issues and Pulls diff --git a/system/core/Exceptions.php b/system/core/Exceptions.php index 7244f3f28ea..3006b690fc8 100644 --- a/system/core/Exceptions.php +++ b/system/core/Exceptions.php @@ -73,7 +73,11 @@ class CI_Exceptions { E_USER_ERROR => 'User Error', E_USER_WARNING => 'User Warning', E_USER_NOTICE => 'User Notice', - E_STRICT => 'Runtime Notice' + + # 2048 is E_STRICT, but it's deprecated in PHP 8.4. + # We're keeping this here for backwards compatibility. + # If we're on older PHP, E_STRICT errors will be labelled correctly, and if we're on PHP 8.4+, this will be ignored. + 2048 => 'Runtime Notice' ); /** diff --git a/tests/Bootstrap.php b/tests/Bootstrap.php index ada6a5998b7..20fabc85db3 100644 --- a/tests/Bootstrap.php +++ b/tests/Bootstrap.php @@ -1,7 +1,12 @@ =')) { + error_reporting(E_ALL); +} else { + error_reporting(E_ALL | E_STRICT); +} $dir = realpath(dirname(__FILE__)); @@ -80,4 +85,4 @@ class_alias('org\bovigo\vfs\vfsStreamWrapper', 'vfsStreamWrapper'); include_once $dir.'/mocks/autoloader.php'; spl_autoload_register('autoload'); -unset($dir); \ No newline at end of file +unset($dir); From 28467baca4818378f781746945a0304a48509fc2 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Tue, 29 Oct 2024 06:34:00 -0600 Subject: [PATCH 14/68] Fixes a few more PHP 8.4 compatibility issues. --- system/core/Exceptions.php | 27 +++++++++++++++------------ system/libraries/Encryption.php | 8 ++++---- system/libraries/Session/Session.php | 2 +- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/system/core/Exceptions.php b/system/core/Exceptions.php index 3006b690fc8..4c3b63e4e97 100644 --- a/system/core/Exceptions.php +++ b/system/core/Exceptions.php @@ -62,22 +62,25 @@ class CI_Exceptions { * @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_ERROR => 'Error', + E_RECOVERABLE_ERROR => 'Recoverable Error', + E_WARNING => 'Warning', + E_PARSE => 'Parsing Error', + E_NOTICE => 'Notice', + E_DEPRECATED => 'Deprecated 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_USER_DEPRECATED => 'User Deprecated Notice', # 2048 is E_STRICT, but it's deprecated in PHP 8.4. # We're keeping this here for backwards compatibility. # If we're on older PHP, E_STRICT errors will be labelled correctly, and if we're on PHP 8.4+, this will be ignored. - 2048 => 'Runtime Notice' + 2048 => 'Strict Notice', ); /** diff --git a/system/libraries/Encryption.php b/system/libraries/Encryption.php index 572cab3fcb1..a457003fe29 100644 --- a/system/libraries/Encryption.php +++ b/system/libraries/Encryption.php @@ -366,10 +366,10 @@ public function create_key($length) * Encrypt * * @param string $data Input data - * @param array $params Input parameters + * @param array|null $params Input parameters * @return string */ - public function encrypt($data, array $params = NULL) + public function encrypt($data, $params = NULL) { if (($params = $this->_get_params($params)) === FALSE) { @@ -501,10 +501,10 @@ protected function _openssl_encrypt($data, $params) * Decrypt * * @param string $data Encrypted data - * @param array $params Input parameters + * @param array|null $params Input parameters * @return string */ - public function decrypt($data, array $params = NULL) + public function decrypt($data, $params = NULL) { if (($params = $this->_get_params($params)) === FALSE) { diff --git a/system/libraries/Session/Session.php b/system/libraries/Session/Session.php index 2d55f822af9..e6715271254 100644 --- a/system/libraries/Session/Session.php +++ b/system/libraries/Session/Session.php @@ -417,7 +417,7 @@ protected function _configure_sid_length() { $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) + if (($bits = $sid_length * $bits_per_character) < 160 && version_compare(PHP_VERSION, '8.4', '<')) { // Add as many more characters as necessary to reach at least 160 bits $sid_length += (int) ceil((160 % $bits) / $bits_per_character); From ba2bafcdc877e68e26f662f7945ab7ced39e2d00 Mon Sep 17 00:00:00 2001 From: P2GR Date: Sat, 21 Dec 2024 15:37:52 +0100 Subject: [PATCH 15/68] Improve timezones() return type. timezones() can return either a specific timezone or the whole array. --- system/helpers/date_helper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/helpers/date_helper.php b/system/helpers/date_helper.php index 6ea9d82bd98..12fa872e321 100644 --- a/system/helpers/date_helper.php +++ b/system/helpers/date_helper.php @@ -501,7 +501,7 @@ function timezone_menu($default = 'UTC', $class = '', $name = 'timezones', $attr * for various other ones in this library * * @param string timezone - * @return string + * @return string|array */ function timezones($tz = '') { From 59d8cffe95733643c499cac54135454ddf8bab3b Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 13 Sep 2025 06:59:39 -0600 Subject: [PATCH 16/68] Fix #28: Fix Postgres DB forge deprecation notice. --- system/database/drivers/postgre/postgre_forge.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/database/drivers/postgre/postgre_forge.php b/system/database/drivers/postgre/postgre_forge.php index 2857fd51727..291a705baf4 100644 --- a/system/database/drivers/postgre/postgre_forge.php +++ b/system/database/drivers/postgre/postgre_forge.php @@ -87,7 +87,7 @@ public function __construct(&$db) if (version_compare($this->db->version(), '9.0', '>')) { - $this->create_table_if = 'CREATE TABLE IF NOT EXISTS'; + $this->_create_table_if = 'CREATE TABLE IF NOT EXISTS'; } } From 9f7bec3eb55557cd31c644da88781114f9722407 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 13 Sep 2025 07:08:56 -0600 Subject: [PATCH 17/68] Fix #25: Resolve dynamic property deprecation notice for CI models. --- system/core/Model.php | 1 + 1 file changed, 1 insertion(+) diff --git a/system/core/Model.php b/system/core/Model.php index b2bbbd4d484..5ca24e0a46f 100644 --- a/system/core/Model.php +++ b/system/core/Model.php @@ -47,6 +47,7 @@ * @author EllisLab Dev Team * @link https://codeigniter.com/userguide3/libraries/config.html */ +#[AllowDynamicProperties] class CI_Model { /** From e87d3832270fd26f72e4c249dfcdbe19b7dfb239 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 13 Sep 2025 07:12:11 -0600 Subject: [PATCH 18/68] Fix #27: Clarify installation instructions and update CI_VERSION to fit the new release. --- readme.rst | 27 ++++++++++++++++++++++++++- system/core/CodeIgniter.php | 2 +- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/readme.rst b/readme.rst index e0100b8f32e..d596f003c8c 100644 --- a/readme.rst +++ b/readme.rst @@ -24,8 +24,33 @@ PHP version 5.4 or newer, same as the original CI3 requirements. Installation ************ -Simple: Just use composer. +You can install this fork using Composer: .. code-block:: bash composer require pocketarc/codeigniter + +After installation, you need to point CodeIgniter to the new system directory. In your `index.php` file, update the `$system_path` variable: + +.. code-block:: php + + $system_path = 'vendor/pocketarc/codeigniter/system'; + +**Alternative Installation (Manual)** + +If you prefer the traditional approach of replacing the system directory: + +1. Download this repository +2. Replace your existing `system/` directory with the one from this fork +3. No changes to `index.php` are needed with this method + +**Note:** The Composer method makes future updates easier with `composer update`, while the manual method requires downloading and replacing the system directory each time. + +**Upgrading from Original CI3** + +If you're migrating from the original CodeIgniter 3.x: + +1. Install via Composer as shown above +2. Update the `$system_path` in your `index.php` +3. Your existing `application/` directory remains unchanged +4. Test thoroughly with your PHP version (especially if using PHP 8.2+) diff --git a/system/core/CodeIgniter.php b/system/core/CodeIgniter.php index 87dd868f966..e52263e7032 100644 --- a/system/core/CodeIgniter.php +++ b/system/core/CodeIgniter.php @@ -56,7 +56,7 @@ * @var string * */ - const CI_VERSION = '3.2.0-dev'; + const CI_VERSION = '3.3.1'; /* * ------------------------------------------------------ From 62c846360e8ba3cce6ced5a242b7f611242243ba Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 13 Sep 2025 07:17:38 -0600 Subject: [PATCH 19/68] Fix #24: Fix deprecation notice in db->join(). --- system/database/DB_query_builder.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/system/database/DB_query_builder.php b/system/database/DB_query_builder.php index c8592352818..9c8580980ff 100644 --- a/system/database/DB_query_builder.php +++ b/system/database/DB_query_builder.php @@ -526,6 +526,14 @@ public function from($from) */ public function join($table, $cond, $type = '', $escape = NULL) { + if ($type === null) { + $type = ''; + } + + if (!is_string($type)) { + throw new InvalidArgumentException('join() expects parameter 3 to be a string, ' . gettype($type) . ' given'); + } + $type = trim(strtoupper($type).' JOIN'); preg_match('#^(NATURAL\s+)?((LEFT|RIGHT|FULL)\s+)?((INNER|OUTER)\s+)?JOIN$#', $type) OR $type = 'JOIN'; From b008b9ad5e29f6405248e0b2b47c61b295c5c859 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 13 Sep 2025 07:19:40 -0600 Subject: [PATCH 20/68] Fix #23: Fix deprecation notice in db->order_by(). --- system/database/DB_query_builder.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/system/database/DB_query_builder.php b/system/database/DB_query_builder.php index 9c8580980ff..feb5f8cbee3 100644 --- a/system/database/DB_query_builder.php +++ b/system/database/DB_query_builder.php @@ -1281,6 +1281,14 @@ public function or_having($key, $value = NULL, $escape = NULL) */ public function order_by($orderby, $direction = '', $escape = NULL) { + if ($direction === null) { + $direction = ''; + } + + if (!is_string($direction)) { + throw new InvalidArgumentException('order_by() expects parameter 2 to be a string, ' . gettype($direction) . ' given'); + } + $direction = strtoupper(trim($direction)); if ($direction === 'RANDOM') From d68db8c149ba68738c314a05a556a537cb9fbc44 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 13 Sep 2025 07:33:21 -0600 Subject: [PATCH 21/68] Fix #19: Fix PDO Postgres DB forge deprecation notice. --- system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php b/system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php index cea20542715..305534253aa 100644 --- a/system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php +++ b/system/database/drivers/pdo/subdrivers/pdo_pgsql_forge.php @@ -99,7 +99,7 @@ public function __construct(&$db) if (version_compare($this->db->version(), '9.0', '>')) { - $this->create_table_if = 'CREATE TABLE IF NOT EXISTS'; + $this->_create_table_if = 'CREATE TABLE IF NOT EXISTS'; } } From f04361707b9642673fa37426a27158aa8efcffde Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 13 Sep 2025 07:46:20 -0600 Subject: [PATCH 22/68] Fix #16: Add the email helper back to the repo to reduce backward compatibility issues. --- system/helpers/email_helper.php | 85 +++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 system/helpers/email_helper.php diff --git a/system/helpers/email_helper.php b/system/helpers/email_helper.php new file mode 100644 index 00000000000..ec0c4207ed9 --- /dev/null +++ b/system/helpers/email_helper.php @@ -0,0 +1,85 @@ + Date: Sat, 13 Sep 2025 07:49:26 -0600 Subject: [PATCH 23/68] Add wording to the README explaining this is based on CI 3.2. --- readme.rst | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/readme.rst b/readme.rst index d596f003c8c..3a21e843144 100644 --- a/readme.rst +++ b/readme.rst @@ -48,9 +48,15 @@ If you prefer the traditional approach of replacing the system directory: **Upgrading from Original CI3** -If you're migrating from the original CodeIgniter 3.x: +⚠️ **Important:** This fork is based on the unreleased CodeIgniter 3.2.0-dev version, not the stable 3.1.13. If you're upgrading from CI 3.1.x, please read the upgrade guide for any changes that may affect your application. -1. Install via Composer as shown above -2. Update the `$system_path` in your `index.php` -3. Your existing `application/` directory remains unchanged -4. Test thoroughly with your PHP version (especially if using PHP 8.2+) +**Please review the upgrade guide:** `upgrade_320.rst `_ + +Steps to upgrade: + +1. Review the upgrade guide for breaking changes between 3.1.x and 3.2.0 +2. Install via Composer as shown above +3. Update the `$system_path` in your `index.php` +4. Apply any necessary changes from the upgrade guide to your application +5. Your existing `application/` directory remains mostly unchanged (except for items noted in the upgrade guide) +6. Test thoroughly with your PHP version (especially if using PHP 8.2+) From a2d200ec6b6115febec7f0288443b460486b07b8 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 13 Sep 2025 07:49:38 -0600 Subject: [PATCH 24/68] Add PHP 8.3/8.4/8.5 to the test matrix in CI. --- .github/workflows/test-phpunit.yml | 305 +++++++++++++++++------------ 1 file changed, 178 insertions(+), 127 deletions(-) diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml index d965765bce7..22fcee6c761 100644 --- a/.github/workflows/test-phpunit.yml +++ b/.github/workflows/test-phpunit.yml @@ -1,140 +1,191 @@ name: PHPUnit -on: [push, pull_request] +on: [ push, pull_request ] permissions: - contents: read + contents: read jobs: - tests: - runs-on: ubuntu-22.04 - if: "!contains(github.event.head_commit.message, '[ci skip]')" - env: - PHP_INI_VALUES: assert.exception=1, zend.assertions=1 + tests: + runs-on: ubuntu-22.04 + if: "!contains(github.event.head_commit.message, '[ci skip]')" + env: + PHP_INI_VALUES: assert.exception=1, zend.assertions=1 - strategy: - fail-fast: false - matrix: - php: [ '8.2', '8.1', '8.0', '7.4', '7.3', '7.2', '7.1', '7.0', '5.6', '5.5', '5.4'] - DB: [ 'pdo/mysql', 'pdo/pgsql', 'pdo/sqlite', 'mysqli', 'pgsql', 'sqlite' ] - compiler: [ default ] - include: - - php: '8.2' - DB: 'pdo/mysql' - compiler: jit - - php: '8.2' - DB: 'pdo/pgsql' - compiler: jit - - php: '8.2' - DB: 'pdo/sqlite' - compiler: jit - - php: '8.2' - DB: 'mysqli' - compiler: jit - - php: '8.2' - DB: 'pgsql' - compiler: jit - - php: '8.2' - DB: 'sqlite' - compiler: jit - - php: '8.1' - DB: 'pdo/mysql' - compiler: jit - - php: '8.1' - DB: 'pdo/pgsql' - compiler: jit - - php: '8.1' - DB: 'pdo/sqlite' - compiler: jit - - php: '8.1' - DB: 'mysqli' - compiler: jit - - php: '8.1' - DB: 'pgsql' - compiler: jit - - php: '8.1' - DB: 'sqlite' - compiler: jit - - php: '8.0' - DB: 'pdo/mysql' - compiler: jit - - php: '8.0' - DB: 'pdo/pgsql' - compiler: jit - - php: '8.0' - DB: 'pdo/sqlite' - compiler: jit - - php: '8.0' - DB: 'mysqli' - compiler: jit - - php: '8.0' - DB: 'pgsql' - compiler: jit - - php: '8.0' - DB: 'sqlite' - compiler: jit - - php: '5.6' - DB: 'mysql' - compiler: default - - php: '5.5' - DB: 'mysql' - compiler: default - - php: '5.4' - DB: 'mysql' - compiler: default + strategy: + fail-fast: false + matrix: + php: [ '8.5', 8.4', '8.3', '8.2', '8.1', '8.0', '7.4', '7.3', '7.2', '7.1', '7.0', '5.6', '5.5', '5.4' ] + DB: [ 'pdo/mysql', 'pdo/pgsql', 'pdo/sqlite', 'mysqli', 'pgsql', 'sqlite' ] + compiler: [ default ] + include: + - php: '8.5' + DB: 'pdo/mysql' + compiler: jit + - php: '8.5' + DB: 'pdo/pgsql' + compiler: jit + - php: '8.5' + DB: 'pdo/sqlite' + compiler: jit + - php: '8.5' + DB: 'mysqli' + compiler: jit + - php: '8.5' + DB: 'pgsql' + compiler: jit + - php: '8.5' + DB: 'sqlite' + - php: '8.4' + DB: 'pdo/mysql' + compiler: jit + - php: '8.4' + DB: 'pdo/pgsql' + compiler: jit + - php: '8.4' + DB: 'pdo/sqlite' + compiler: jit + - php: '8.4' + DB: 'mysqli' + compiler: jit + - php: '8.4' + DB: 'pgsql' + compiler: jit + - php: '8.4' + DB: 'sqlite' + - php: '8.3' + DB: 'pdo/mysql' + compiler: jit + - php: '8.3' + DB: 'pdo/pgsql' + compiler: jit + - php: '8.3' + DB: 'pdo/sqlite' + compiler: jit + - php: '8.3' + DB: 'mysqli' + compiler: jit + - php: '8.3' + DB: 'pgsql' + compiler: jit + - php: '8.3' + DB: 'sqlite' + - php: '8.2' + DB: 'pdo/mysql' + compiler: jit + - php: '8.2' + DB: 'pdo/pgsql' + compiler: jit + - php: '8.2' + DB: 'pdo/sqlite' + compiler: jit + - php: '8.2' + DB: 'mysqli' + compiler: jit + - php: '8.2' + DB: 'pgsql' + compiler: jit + - php: '8.2' + DB: 'sqlite' + compiler: jit + - php: '8.1' + DB: 'pdo/mysql' + compiler: jit + - php: '8.1' + DB: 'pdo/pgsql' + compiler: jit + - php: '8.1' + DB: 'pdo/sqlite' + compiler: jit + - php: '8.1' + DB: 'mysqli' + compiler: jit + - php: '8.1' + DB: 'pgsql' + compiler: jit + - php: '8.1' + DB: 'sqlite' + compiler: jit + - php: '8.0' + DB: 'pdo/mysql' + compiler: jit + - php: '8.0' + DB: 'pdo/pgsql' + compiler: jit + - php: '8.0' + DB: 'pdo/sqlite' + compiler: jit + - php: '8.0' + DB: 'mysqli' + compiler: jit + - php: '8.0' + DB: 'pgsql' + compiler: jit + - php: '8.0' + DB: 'sqlite' + compiler: jit + - php: '5.6' + DB: 'mysql' + compiler: default + - php: '5.5' + DB: 'mysql' + compiler: default + - php: '5.4' + DB: 'mysql' + compiler: default - services: - postgres: - image: postgres:12 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: ci_test - ports: - - 5432:5432 - options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3 + services: + postgres: + image: postgres:12 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: ci_test + ports: + - 5432:5432 + options: --health-cmd=pg_isready --health-interval=10s --health-timeout=5s --health-retries=3 - mysql: - image: mysql:5.7 - env: - MYSQL_ALLOW_EMPTY_PASSWORD: true - MYSQL_USER: travis - MYSQL_PASSWORD: travis - MYSQL_DATABASE: ci_test - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + mysql: + image: mysql:5.7 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: true + MYSQL_USER: travis + MYSQL_PASSWORD: travis + MYSQL_DATABASE: ci_test + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Override PHP ini values for JIT compiler - if: matrix.compiler == 'jit' - run: echo "PHP_INI_VALUES::assert.exception=1, zend.assertions=1, opcache.enable=1, opcache.enable_cli=1, opcache.optimization_level=-1, opcache.jit=1255, opcache.jit_buffer_size=64M" >> $GITHUB_ENV + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Override PHP ini values for JIT compiler + if: matrix.compiler == 'jit' + run: echo "PHP_INI_VALUES::assert.exception=1, zend.assertions=1, opcache.enable=1, opcache.enable_cli=1, opcache.optimization_level=-1, opcache.jit=1255, opcache.jit_buffer_size=64M" >> $GITHUB_ENV - - name: Install PHP${{ matrix.php }} - DB ${{ matrix.DB }} - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - tools: composer, pecl - extensions: imagick, sqlite3, pgsql, mysqli, pdo, pdo_mysql, pdo_pgsql, pdo_sqlite, mbstring - ini-values: ${{ env.PHP_INI_VALUES }} - coverage: xdebug + - name: Install PHP${{ matrix.php }} - DB ${{ matrix.DB }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer, pecl + extensions: imagick, sqlite3, pgsql, mysqli, pdo, pdo_mysql, pdo_pgsql, pdo_sqlite, mbstring + ini-values: ${{ env.PHP_INI_VALUES }} + coverage: xdebug - - name: Get composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - name: Cache composer dependencies - uses: actions/cache@v2 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ runner.os }}-composer- - - name: Install composer dependencies - run: composer install --no-progress --prefer-dist --optimize-autoloader + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install composer dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader - - name: PHPUnit Test - run: | - php -d error_reporting=E_ALL -d zend.enable_gc=0 -d date.timezone=UTC -d mbstring.func_overload=7 -d mbstring.internal_encoding=UTF-8 vendor/bin/phpunit --coverage-text --configuration tests/travis/${{ matrix.DB }}.phpunit.xml - env: - XDEBUG_MODE: coverage + - name: PHPUnit Test + run: | + php -d error_reporting=E_ALL -d zend.enable_gc=0 -d date.timezone=UTC -d mbstring.func_overload=7 -d mbstring.internal_encoding=UTF-8 vendor/bin/phpunit --coverage-text --configuration tests/travis/${{ matrix.DB }}.phpunit.xml + env: + XDEBUG_MODE: coverage From 1be9a6c41f32e3cfef0b391a88c1ad902ffe7ca5 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 13 Sep 2025 07:58:55 -0600 Subject: [PATCH 25/68] Update readme to clarify version support and maintenance policy. --- readme.rst | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/readme.rst b/readme.rst index 3a21e843144..10ba1d89d20 100644 --- a/readme.rst +++ b/readme.rst @@ -2,12 +2,39 @@ What is this repository? ######################## -This is a fork of CodeIgniter 3, with the goal of keeping it up to date with **PHP 8.4** and beyond. There is no intention to add new features or change the way CI3 works. This is purely a maintenance fork. +This is a fork of CodeIgniter 3, with the goal of keeping it up to date with modern PHP versions. There is no intention to add new features or change the way CI3 works. This is purely a maintenance fork. + +**PHP Compatibility:** + +- ✅ PHP 5.4 - 8.1 (as per original CI3 support) +- ✅ PHP 8.2 +- ✅ PHP 8.3 +- ✅ PHP 8.4 +- ✅ PHP 8.5 (and beyond as they are released) The original CodeIgniter 3.x branch is no longer maintained, and has not been updated to work with PHP 8.2, or any newer version. This fork is intended to fill that gap. If the original CodeIgniter 3.x branch is updated to work with PHP 8.2+, and starts to be maintained again, this fork might be retired. +******************** +Maintenance Policy +******************** + +This fork commits to: + +- Maintaining compatibility with each new PHP version while still supporting PHP 5.4+ +- Applying critical security fixes +- Keeping changes minimal to preserve CI3 behavior +- Reverting unnecessary breaking changes in CodeIgniter 3.2.0-dev +- Running the full CI3 test suite on PHP 8.2+ + +This fork does NOT: + +- Add new features +- Change existing CI3 behavior +- Provide commercial support +- Make migration to CI4 any harder (or easier) + **************** Issues and Pulls **************** From 7a583c53e669c1b8a7644c2c89779ff7064be693 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 13 Sep 2025 08:53:22 -0600 Subject: [PATCH 26/68] Fix PHP 8.5 compatibility issue with MySQL driver. --- .../pdo/subdrivers/pdo_mysql_driver.php | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/system/database/drivers/pdo/subdrivers/pdo_mysql_driver.php b/system/database/drivers/pdo/subdrivers/pdo_mysql_driver.php index 1ad854da154..d3a24ae58d7 100644 --- a/system/database/drivers/pdo/subdrivers/pdo_mysql_driver.php +++ b/system/database/drivers/pdo/subdrivers/pdo_mysql_driver.php @@ -143,34 +143,65 @@ public function db_connect($persistent = FALSE) if ( ! empty($sql)) { - if (empty($this->options[PDO::MYSQL_ATTR_INIT_COMMAND])) + if (class_exists('Pdo\\Mysql')) { - $this->options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET SESSION sql_mode = '.$sql; + $constant = constant('Pdo\\Mysql::ATTR_INIT_COMMAND'); + } else { + $constant = PDO::MYSQL_ATTR_INIT_COMMAND; + } + + if (empty($this->options[$constant])) + { + $this->options[$constant] = 'SET SESSION sql_mode = '.$sql; } else { - $this->options[PDO::MYSQL_ATTR_INIT_COMMAND] .= ', @@session.sql_mode = '.$sql; + $this->options[$constant] .= ', @@session.sql_mode = '.$sql; } } } if ($this->compress === TRUE) { - $this->options[PDO::MYSQL_ATTR_COMPRESS] = TRUE; + if (class_exists('Pdo\\Mysql')) + { + $constant = constant('Pdo\\Mysql::ATTR_COMPRESS'); + } else { + $constant = PDO::MYSQL_ATTR_COMPRESS; + } + + $this->options[$constant] = TRUE; } if (is_array($this->encrypt)) { + if (class_exists('Pdo\\Mysql')) + { + $sslKey = constant('Pdo\\Mysql::ATTR_SSL_KEY'); + $sslCert = constant('Pdo\\Mysql::ATTR_SSL_CERT'); + $sslCA = constant('Pdo\\Mysql::ATTR_SSL_CA'); + $sslCAPath = constant('Pdo\\Mysql::ATTR_SSL_CAPATH'); + $sslCipher = constant('Pdo\\Mysql::ATTR_SSL_CIPHER'); + $verify = constant('Pdo\\Mysql::ATTR_SSL_VERIFY_SERVER_CERT'); + } else { + $sslKey = PDO::MYSQL_ATTR_SSL_KEY; + $sslCert = PDO::MYSQL_ATTR_SSL_CERT; + $sslCA = PDO::MYSQL_ATTR_SSL_CA; + $sslCAPath = PDO::MYSQL_ATTR_SSL_CAPATH; + $sslCipher = PDO::MYSQL_ATTR_SSL_CIPHER; + $verify = defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') ? PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT : null; + } + $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']; + empty($this->encrypt['ssl_key']) OR $ssl[$sslKey] = $this->encrypt['ssl_key']; + empty($this->encrypt['ssl_cert']) OR $ssl[$sslCert] = $this->encrypt['ssl_cert']; + empty($this->encrypt['ssl_ca']) OR $ssl[$sslCA] = $this->encrypt['ssl_ca']; + empty($this->encrypt['ssl_capath']) OR $ssl[$sslCAPath] = $this->encrypt['ssl_capath']; + empty($this->encrypt['ssl_cipher']) OR $ssl[$sslCipher] = $this->encrypt['ssl_cipher']; - if (defined('PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT') && isset($this->encrypt['ssl_verify'])) + if ($verify && isset($this->encrypt['ssl_verify'])) { - $ssl[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $this->encrypt['ssl_verify']; + $ssl[$verify] = $this->encrypt['ssl_verify']; } // DO NOT use array_merge() here! From 91bb1b5773503bae26f2803cb47736f1a1f189e9 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 07:10:46 -0700 Subject: [PATCH 27/68] fix: remove deprecated MYSQLI_TYPE_INTERVAL constant for PHP 8.4+ compatibility. --- system/database/drivers/mysqli/mysqli_result.php | 1 - 1 file changed, 1 deletion(-) diff --git a/system/database/drivers/mysqli/mysqli_result.php b/system/database/drivers/mysqli/mysqli_result.php index 8c4f94d18fe..5c0c8673978 100644 --- a/system/database/drivers/mysqli/mysqli_result.php +++ b/system/database/drivers/mysqli/mysqli_result.php @@ -153,7 +153,6 @@ private static function _get_field_type($type) 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', From baeaad2e436a38cc252899bf4594dea7ea7c93d8 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 07:23:36 -0700 Subject: [PATCH 28/68] fix(upload): add PHP 8.1+ compatibility for finfo_close() deprecation. finfo_open() returns an finfo object (not a resource) since PHP 8.1, which is automatically freed when it goes out of scope. Fixes #34. --- system/libraries/Upload.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/system/libraries/Upload.php b/system/libraries/Upload.php index 168211a9144..26bed6e5f5d 100644 --- a/system/libraries/Upload.php +++ b/system/libraries/Upload.php @@ -1231,7 +1231,10 @@ protected function _file_mime_type($file) 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); + if (PHP_VERSION_ID < 80100) + { + finfo_close($finfo); + } /* According to the comments section of the PHP manual page, * it is possible that this function returns an empty string From 91a3d809e4b173d5f8e4d4dcbff5d6e4279041b2 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 07:32:09 -0700 Subject: [PATCH 29/68] fix: handle null values in Encryption library for PHP 8.1+ compatibility. Closes #29. --- system/libraries/Encryption.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/system/libraries/Encryption.php b/system/libraries/Encryption.php index a457003fe29..b53dac2e830 100644 --- a/system/libraries/Encryption.php +++ b/system/libraries/Encryption.php @@ -470,6 +470,11 @@ protected function _mcrypt_encrypt($data, $params) */ protected function _openssl_encrypt($data, $params) { + if ($data === NULL) + { + return NULL; + } + if (empty($params['handle'])) { return FALSE; @@ -626,6 +631,11 @@ protected function _mcrypt_decrypt($data, $params) */ protected function _openssl_decrypt($data, $params) { + if ($data === NULL) + { + return NULL; + } + if ($iv_size = openssl_cipher_iv_length($params['handle'])) { $iv = self::substr($data, 0, $iv_size); From 93cd584bd44ed17b8a32d0ac251bafe455fd581c Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 09:40:45 -0500 Subject: [PATCH 30/68] Merge gxgpet's XML-RPC fixes. * Fixes XML-RPC compatibility with PHP 8 * Adds PHPUnit test for XML-RPC lib --------- Co-authored-by: George Petculescu --- system/libraries/Xmlrpc.php | 161 ++++++++++---------- system/libraries/Xmlrpcs.php | 13 +- tests/codeigniter/libraries/Xmlrpc_test.php | 130 ++++++++++++++++ tests/mocks/autoloader.php | 1 + tests/mocks/libraries/xmlrpc.php | 33 ++++ tests/mocks/libraries/xmlrpcs.php | 23 +++ 6 files changed, 270 insertions(+), 91 deletions(-) create mode 100644 tests/codeigniter/libraries/Xmlrpc_test.php create mode 100644 tests/mocks/libraries/xmlrpc.php create mode 100644 tests/mocks/libraries/xmlrpcs.php diff --git a/system/libraries/Xmlrpc.php b/system/libraries/Xmlrpc.php index 8587015e877..5238c056f9a 100644 --- a/system/libraries/Xmlrpc.php +++ b/system/libraries/Xmlrpc.php @@ -1151,8 +1151,7 @@ public function parseResponse($fp) //------------------------------------- $parser = xml_parser_create($this->xmlrpc_defencoding); - $pname = (string) $parser; - $this->xh[$pname] = array( + $this->xh = array( 'isf' => 0, 'ac' => '', 'headers' => array(), @@ -1175,7 +1174,7 @@ public function parseResponse($fp) { break; } - $this->xh[$pname]['headers'][] = $line; + $this->xh['headers'][] = $line; } $data = implode("\r\n", $lines); @@ -1193,18 +1192,18 @@ public function parseResponse($fp) xml_parser_free($parser); // Got ourselves some badness, it seems - if ($this->xh[$pname]['isf'] > 1) + if ($this->xh['isf'] > 1) { if ($this->debug === TRUE) { - echo "---Invalid Return---\n".$this->xh[$pname]['isf_reason']."---Invalid Return---\n\n"; + echo "---Invalid Return---\n".$this->xh['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']); + return new XML_RPC_Response(0, $this->xmlrpcerr['invalid_return'], $this->xmlrpcstr['invalid_return'].' '.$this->xh['isf_reason']); } - elseif ( ! is_object($this->xh[$pname]['value'])) + elseif ( ! is_object($this->xh['value'])) { - return new XML_RPC_Response(0, $this->xmlrpcerr['invalid_return'], $this->xmlrpcstr['invalid_return'].' '.$this->xh[$pname]['isf_reason']); + return new XML_RPC_Response(0, $this->xmlrpcerr['invalid_return'], $this->xmlrpcstr['invalid_return'].' '.$this->xh['isf_reason']); } // Display XML content for debugging @@ -1212,10 +1211,10 @@ public function parseResponse($fp) { echo '
';
 
-			if (count($this->xh[$pname]['headers']) > 0)
+			if (count($this->xh['headers']) > 0)
 			{
 				echo "---HEADERS---\n";
-				foreach ($this->xh[$pname]['headers'] as $header)
+				foreach ($this->xh['headers'] as $header)
 				{
 					echo $header."\n";
 				}
@@ -1223,13 +1222,13 @@ public function parseResponse($fp)
 			}
 
 			echo "---DATA---\n".htmlspecialchars($data)."\n---END DATA---\n\n---PARSED---\n";
-			var_dump($this->xh[$pname]['value']);
+			var_dump($this->xh['value']);
 			echo "\n---END PARSED---
"; } // Send response - $v = $this->xh[$pname]['value']; - if ($this->xh[$pname]['isf']) + $v = $this->xh['value']; + if ($this->xh['isf']) { $errno_v = $v->me['struct']['faultCode']; $errstr_v = $v->me['struct']['faultString']; @@ -1248,7 +1247,7 @@ public function parseResponse($fp) $r = new XML_RPC_Response($v); } - $r->headers = $this->xh[$pname]['headers']; + $r->headers = $this->xh['headers']; return $r; } @@ -1279,26 +1278,24 @@ public function parseResponse($fp) */ 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; + if ($this->xh['isf'] > 1) return; // Evaluate and check for correct nesting of XML elements - if (count($this->xh[$the_parser]['stack']) === 0) + if (count($this->xh['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'; + $this->xh['isf'] = 2; + $this->xh['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)) + elseif ( ! in_array($this->xh['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]; + $this->xh['isf'] = 2; + $this->xh['isf_reason'] = 'XML-RPC element '.$name.' cannot be child of '.$this->xh['stack'][0]; return; } @@ -1308,22 +1305,22 @@ public function open_tag($the_parser, $name) case 'ARRAY': // Creates array for child elements $cur_val = array('value' => array(), 'type' => $name); - array_unshift($this->xh[$the_parser]['valuestack'], $cur_val); + array_unshift($this->xh['valuestack'], $cur_val); break; case 'METHODNAME': case 'NAME': - $this->xh[$the_parser]['ac'] = ''; + $this->xh['ac'] = ''; break; case 'FAULT': - $this->xh[$the_parser]['isf'] = 1; + $this->xh['isf'] = 1; break; case 'PARAM': - $this->xh[$the_parser]['value'] = NULL; + $this->xh['value'] = NULL; break; case 'VALUE': - $this->xh[$the_parser]['vt'] = 'value'; - $this->xh[$the_parser]['ac'] = ''; - $this->xh[$the_parser]['lv'] = 1; + $this->xh['vt'] = 'value'; + $this->xh['ac'] = ''; + $this->xh['lv'] = 1; break; case 'I4': case 'INT': @@ -1332,23 +1329,23 @@ public function open_tag($the_parser, $name) case 'DOUBLE': case 'DATETIME.ISO8601': case 'BASE64': - if ($this->xh[$the_parser]['vt'] !== 'value') + if ($this->xh['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'; + $this->xh['isf'] = 2; + $this->xh['isf_reason'] = 'There is a '.$name.' element following a ' + .$this->xh['vt'].' element inside a single value'; return; } - $this->xh[$the_parser]['ac'] = ''; + $this->xh['ac'] = ''; break; case 'MEMBER': // Set name of to nothing to prevent errors later if no is found - $this->xh[$the_parser]['valuestack'][0]['name'] = ''; + $this->xh['valuestack'][0]['name'] = ''; // Set NULL value to check to see if value passed for this param/member - $this->xh[$the_parser]['value'] = NULL; + $this->xh['value'] = NULL; break; case 'DATA': case 'METHODCALL': @@ -1358,15 +1355,15 @@ public function open_tag($the_parser, $name) 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; + $this->xh['isf'] = 2; + $this->xh['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); + array_unshift($this->xh['stack'], $name); - $name === 'VALUE' OR $this->xh[$the_parser]['lv'] = 0; + $name === 'VALUE' OR $this->xh['lv'] = 0; } // -------------------------------------------------------------------- @@ -1380,27 +1377,25 @@ public function open_tag($the_parser, $name) */ public function closing_tag($the_parser, $name) { - $the_parser = (string) $the_parser; - - if ($this->xh[$the_parser]['isf'] > 1) return; + if ($this->xh['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']); + $curr_elem = array_shift($this->xh['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); + $cur_val = array_shift($this->xh['valuestack']); + $this->xh['value'] = isset($cur_val['values']) ? $cur_val['values'] : array(); + $this->xh['vt'] = strtolower($name); break; case 'NAME': - $this->xh[$the_parser]['valuestack'][0]['name'] = $this->xh[$the_parser]['ac']; + $this->xh['valuestack'][0]['name'] = $this->xh['ac']; break; case 'BOOLEAN': case 'I4': @@ -1409,87 +1404,87 @@ public function closing_tag($the_parser, $name) case 'DOUBLE': case 'DATETIME.ISO8601': case 'BASE64': - $this->xh[$the_parser]['vt'] = strtolower($name); + $this->xh['vt'] = strtolower($name); if ($name === 'STRING') { - $this->xh[$the_parser]['value'] = $this->xh[$the_parser]['ac']; + $this->xh['value'] = $this->xh['ac']; } elseif ($name === 'DATETIME.ISO8601') { - $this->xh[$the_parser]['vt'] = $this->xmlrpcDateTime; - $this->xh[$the_parser]['value'] = $this->xh[$the_parser]['ac']; + $this->xh['vt'] = $this->xmlrpcDateTime; + $this->xh['value'] = $this->xh['ac']; } elseif ($name === 'BASE64') { - $this->xh[$the_parser]['value'] = base64_decode($this->xh[$the_parser]['ac']); + $this->xh['value'] = base64_decode($this->xh['ac']); } elseif ($name === 'BOOLEAN') { // Translated BOOLEAN values to TRUE AND FALSE - $this->xh[$the_parser]['value'] = (bool) $this->xh[$the_parser]['ac']; + $this->xh['value'] = (bool) $this->xh['ac']; } elseif ($name=='DOUBLE') { // we have a DOUBLE // we must check that only 0123456789-. are characters here - $this->xh[$the_parser]['value'] = preg_match('/^[+-]?[eE0-9\t \.]+$/', $this->xh[$the_parser]['ac']) - ? (float) $this->xh[$the_parser]['ac'] + $this->xh['value'] = preg_match('/^[+-]?[eE0-9\t \.]+$/', $this->xh['ac']) + ? (float) $this->xh['ac'] : 'ERROR_NON_NUMERIC_FOUND'; } else { // we have an I4/INT // we must check that only 0123456789- are characters here - $this->xh[$the_parser]['value'] = preg_match('/^[+-]?[0-9\t ]+$/', $this->xh[$the_parser]['ac']) - ? (int) $this->xh[$the_parser]['ac'] + $this->xh['value'] = preg_match('/^[+-]?[0-9\t ]+$/', $this->xh['ac']) + ? (int) $this->xh['ac'] : 'ERROR_NON_NUMERIC_FOUND'; } - $this->xh[$the_parser]['ac'] = ''; - $this->xh[$the_parser]['lv'] = 3; // indicate we've found a value + $this->xh['ac'] = ''; + $this->xh['lv'] = 3; // indicate we've found a value break; case 'VALUE': // This if() detects if no scalar was inside - if ($this->xh[$the_parser]['vt'] == 'value') + if ($this->xh['vt'] == 'value') { - $this->xh[$the_parser]['value'] = $this->xh[$the_parser]['ac']; - $this->xh[$the_parser]['vt'] = $this->xmlrpcString; + $this->xh['value'] = $this->xh['ac']; + $this->xh['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']); + $temp = new XML_RPC_Values($this->xh['value'], $this->xh['vt']); - if (count($this->xh[$the_parser]['valuestack']) && $this->xh[$the_parser]['valuestack'][0]['type'] === 'ARRAY') + if (count($this->xh['valuestack']) && $this->xh['valuestack'][0]['type'] === 'ARRAY') { // Array - $this->xh[$the_parser]['valuestack'][0]['values'][] = $temp; + $this->xh['valuestack'][0]['values'][] = $temp; } else { // Struct - $this->xh[$the_parser]['value'] = $temp; + $this->xh['value'] = $temp; } break; case 'MEMBER': - $this->xh[$the_parser]['ac'] = ''; + $this->xh['ac'] = ''; // If value add to array in the stack for the last element built - if ($this->xh[$the_parser]['value']) + if ($this->xh['value']) { - $this->xh[$the_parser]['valuestack'][0]['values'][$this->xh[$the_parser]['valuestack'][0]['name']] = $this->xh[$the_parser]['value']; + $this->xh['valuestack'][0]['values'][$this->xh['valuestack'][0]['name']] = $this->xh['value']; } break; case 'DATA': - $this->xh[$the_parser]['ac'] = ''; + $this->xh['ac'] = ''; break; case 'PARAM': - if ($this->xh[$the_parser]['value']) + if ($this->xh['value']) { - $this->xh[$the_parser]['params'][] = $this->xh[$the_parser]['value']; + $this->xh['params'][] = $this->xh['value']; } break; case 'METHODNAME': - $this->xh[$the_parser]['method'] = ltrim($this->xh[$the_parser]['ac']); + $this->xh['method'] = ltrim($this->xh['ac']); break; case 'PARAMS': case 'FAULT': @@ -1514,24 +1509,22 @@ public function closing_tag($the_parser, $name) */ 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 ($this->xh['isf'] > 1) return; // XML Fault found already // If a value has not been found - if ($this->xh[$the_parser]['lv'] !== 3) + if ($this->xh['lv'] !== 3) { - if ($this->xh[$the_parser]['lv'] === 1) + if ($this->xh['lv'] === 1) { - $this->xh[$the_parser]['lv'] = 2; // Found a value + $this->xh['lv'] = 2; // Found a value } - if ( ! isset($this->xh[$the_parser]['ac'])) + if ( ! isset($this->xh['ac'])) { - $this->xh[$the_parser]['ac'] = ''; + $this->xh['ac'] = ''; } - $this->xh[$the_parser]['ac'] .= $data; + $this->xh['ac'] .= $data; } } diff --git a/system/libraries/Xmlrpcs.php b/system/libraries/Xmlrpcs.php index eb5a24c4917..481938b3400 100644 --- a/system/libraries/Xmlrpcs.php +++ b/system/libraries/Xmlrpcs.php @@ -234,9 +234,8 @@ public function parseRequest($data = '') $parser = xml_parser_create($this->xmlrpc_defencoding); $parser_object = new XML_RPC_Message('filler'); - $pname = (string) $parser; - $parser_object->xh[$pname] = array( + $parser_object->xh = array( 'isf' => 0, 'isf_reason' => '', 'params' => array(), @@ -265,7 +264,7 @@ public function parseRequest($data = '') xml_get_current_line_number($parser))); xml_parser_free($parser); } - elseif ($parser_object->xh[$pname]['isf']) + elseif ($parser_object->xh['isf']) { return new XML_RPC_Response(0, $this->xmlrpcerr['invalid_return'], $this->xmlrpcstr['invalid_return']); } @@ -273,17 +272,17 @@ public function parseRequest($data = '') { xml_parser_free($parser); - $m = new XML_RPC_Message($parser_object->xh[$pname]['method']); + $m = new XML_RPC_Message($parser_object->xh['method']); $plist = ''; - for ($i = 0, $c = count($parser_object->xh[$pname]['params']); $i < $c; $i++) + for ($i = 0, $c = count($parser_object->xh['params']); $i < $c; $i++) { if ($this->debug === TRUE) { - $plist .= $i.' - '.print_r(get_object_vars($parser_object->xh[$pname]['params'][$i]), TRUE).";\n"; + $plist .= $i.' - '.print_r(get_object_vars($parser_object->xh['params'][$i]), TRUE).";\n"; } - $m->addParam($parser_object->xh[$pname]['params'][$i]); + $m->addParam($parser_object->xh['params'][$i]); } if ($this->debug === TRUE) diff --git a/tests/codeigniter/libraries/Xmlrpc_test.php b/tests/codeigniter/libraries/Xmlrpc_test.php new file mode 100644 index 00000000000..46d73bf6e3e --- /dev/null +++ b/tests/codeigniter/libraries/Xmlrpc_test.php @@ -0,0 +1,130 @@ +input = new CI_Input($security); + + $this->input_lib_raw_stream = new ReflectionProperty($this->input, '_raw_input_stream'); + $this->input_lib_raw_stream->setAccessible(TRUE); + + $this->ci_instance_var('input', $this->input); + $this->ci_instance_var('security', $security); + } + + // -------------------------------------------------------------------- + + public function test_xmlrpc_client() + { + $xmlrpc = new Mock_Libraries_Xmlrpc(); + $xmlrpc->server('http://rpc.test/'); + $xmlrpc->method('testcontroller.test'); + + $request = array('My Blog', 'http://www.myrpc.com/test/'); + $message = 'test'.time(); + $xml_response = $this->xml_response($message); + $xmlrpc->client->mock_response = "HTTP/1.1 200 OK\r\nContent-Type: text/xml\r\nContent-Length: ".strlen($xml_response)."\r\n\r\n$xml_response"; + + // Perform in the same request multiple calls + for ($attempt = 1; $attempt <= 2; $attempt++) + { + $xmlrpc->request($request); + + $this->assertTrue($xmlrpc->send_request()); + + $response = $xmlrpc->display_response(); + + $this->assertEquals('theuser', $response['name']); + $this->assertEquals(123435, $response['member_id']); + $this->assertEquals($message, $response['request']); + } + } + + // -------------------------------------------------------------------- + + public function test_xmlrpc_server() + { + $xmlrpcs = new Mock_Libraries_Xmlrpcs(); + + $config['functions']['Testmethod'] = array('function' => __CLASS__.'.mock_method_new_entry'); + $config['object'] = $this; + + $xmlrpcs->initialize($config); + + $_SERVER['REQUEST_METHOD'] = 'POST'; + $this->input_lib_raw_stream->setValue($this->input, $this->xml_request()); + + $xmlrpcs->serve(); + + $this->assertEquals('Test', $this->method_param); + } + + // -------------------------------------------------------------------- + + /** + * @param XML_RPC_Message $param + */ + public function mock_method_new_entry($param) + { + $this->method_param = $param->params[0]->scalarval(); + + return new XML_RPC_Response(new XML_RPC_Values(true, 'boolean')); + } + + // -------------------------------------------------------------------- + + private function xml_response($message) + { + return ' + + + + + + +name + +theuser + + + +member_id + +123435 + + + +request + +'.$message.' + + + + + +'; + } + + // -------------------------------------------------------------------- + + public function xml_request() + { + return ' + +Testmethod + + + +Test + + + +'; + } +} diff --git a/tests/mocks/autoloader.php b/tests/mocks/autoloader.php index 4dd53d4af31..ce3454dc9ed 100644 --- a/tests/mocks/autoloader.php +++ b/tests/mocks/autoloader.php @@ -52,6 +52,7 @@ function autoload($class) 'Upload', 'User_agent', 'Xmlrpc', + 'Xmlrpcs', 'Zip' ); diff --git a/tests/mocks/libraries/xmlrpc.php b/tests/mocks/libraries/xmlrpc.php new file mode 100644 index 00000000000..32f5f3765d5 --- /dev/null +++ b/tests/mocks/libraries/xmlrpc.php @@ -0,0 +1,33 @@ +client = new Mock_Libraries_XML_RPC_Client('/', $url, $port, $proxy, $proxy_port); + } +} + +class Mock_Libraries_XML_RPC_Client extends XML_RPC_Client { + public $mock_response = ''; + + /** + * @param XML_RPC_Message $msg + */ + public function sendPayload($msg) + { + if (empty($msg->payload)) + { + $msg->createPayload(); + } + + $fp = fopen('php://memory', 'rw+'); + fwrite($fp, $this->mock_response); + fseek($fp, 0); + + $parsed = $msg->parseResponse($fp); + fclose($fp); + + return $parsed; + } +} + diff --git a/tests/mocks/libraries/xmlrpcs.php b/tests/mocks/libraries/xmlrpcs.php new file mode 100644 index 00000000000..d93396c0189 --- /dev/null +++ b/tests/mocks/libraries/xmlrpcs.php @@ -0,0 +1,23 @@ +parseRequest(); + + $payload = 'xmlrpc_defencoding.'"?'.'>'."\n".$this->debug_msg.$r->prepare_response(); + + $this->mock_payload = "HTTP/1.1 200 OK\r\n"; + $this->mock_payload .= "Content-Type: text/xml\r\n"; + $this->mock_payload .= 'Content-Length: '.strlen($payload)."\r\n"; + + $this->mock_payload .= "\r\n"; + + $this->mock_payload .= $payload; + } +} From c46e9a28ffd91b47d834b2bd9e17a39411609b6a Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 07:50:53 -0700 Subject: [PATCH 31/68] fix(log): restore log_file_extension config for backward compatibility. Restores support for the deprecated $config['log_file_extension'] option that was removed in upstream CI 3.2.0-dev. Closes #14. --- application/config/config.php | 14 ++++++++++++++ system/core/Log.php | 15 +++++++++++++-- tests/codeigniter/core/Log_test.php | 25 ++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/application/config/config.php b/application/config/config.php index 1e37856fec6..836d9490b3b 100644 --- a/application/config/config.php +++ b/application/config/config.php @@ -237,6 +237,20 @@ */ $config['log_filename'] = ''; +/* +|-------------------------------------------------------------------------- +| Log File Extension (DEPRECATED) +|-------------------------------------------------------------------------- +| +| This option is deprecated. Use 'log_filename' instead. +| Kept for backward compatibility with CI 3.1.x configurations. +| +| If 'log_filename' is set, this option is ignored. +| Example: 'txt' or 'log' (without leading dot) +| +*/ +$config['log_file_extension'] = ''; + /* |-------------------------------------------------------------------------- | Log File Permissions diff --git a/system/core/Log.php b/system/core/Log.php index 99642e0c6a2..efee3fe4ccf 100644 --- a/system/core/Log.php +++ b/system/core/Log.php @@ -128,8 +128,19 @@ public function __construct() $this->_log_path = ($config['log_path'] !== '') ? rtrim($config['log_path'], '/\\').DIRECTORY_SEPARATOR : APPPATH.'logs'.DIRECTORY_SEPARATOR; - $this->_log_filename = (isset($config['log_filename']) && $config['log_filename'] !== '') - ? $config['log_filename'] : 'log-'.date('Y-m-d').'.php'; + if (isset($config['log_filename']) && $config['log_filename'] !== '') + { + $this->_log_filename = $config['log_filename']; + } + elseif (isset($config['log_file_extension']) && $config['log_file_extension'] !== '') + { + $ext = ltrim($config['log_file_extension'], '.'); + $this->_log_filename = 'log-'.date('Y-m-d').'.'.$ext; + } + else + { + $this->_log_filename = 'log-'.date('Y-m-d').'.php'; + } file_exists($this->_log_path) OR mkdir($this->_log_path, 0755, TRUE); diff --git a/tests/codeigniter/core/Log_test.php b/tests/codeigniter/core/Log_test.php index fc3e11e1fa5..3f0717b7920 100644 --- a/tests/codeigniter/core/Log_test.php +++ b/tests/codeigniter/core/Log_test.php @@ -45,7 +45,30 @@ public function test_configuration() $this->assertEquals($enabled->getValue($instance), TRUE); } - // -------------------------------------------------------------------- + public function test_log_file_extension_backward_compatibility() + { + $filename = new ReflectionProperty('CI_Log', '_log_filename'); + $filename->setAccessible(TRUE); + + $this->ci_set_config('log_path', ''); + $this->ci_set_config('log_threshold', 0); + $this->ci_set_config('log_filename', ''); + $this->ci_set_config('log_file_extension', 'txt'); + $instance = new CI_Log(); + + $this->assertEquals($filename->getValue($instance), 'log-'.date('Y-m-d').'.txt'); + + $this->ci_set_config('log_file_extension', '.log'); + $instance = new CI_Log(); + + $this->assertEquals($filename->getValue($instance), 'log-'.date('Y-m-d').'.log'); + + $this->ci_set_config('log_filename', 'custom.log'); + $this->ci_set_config('log_file_extension', 'txt'); + $instance = new CI_Log(); + + $this->assertEquals($filename->getValue($instance), 'custom.log'); + } public function test_format_line() { From ea3b4f981ec5a10712062ed94460d829750cfef0 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 08:16:54 -0700 Subject: [PATCH 32/68] fix(tests): update highlight_code test for PHP 8.4+ compatibility. PHP 8.4 changed highlight_string() output format from with   entities to
 with regular spaces.

---
 tests/codeigniter/helpers/text_helper_test.php | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/tests/codeigniter/helpers/text_helper_test.php b/tests/codeigniter/helpers/text_helper_test.php
index 0713775da74..513f28af197 100644
--- a/tests/codeigniter/helpers/text_helper_test.php
+++ b/tests/codeigniter/helpers/text_helper_test.php
@@ -101,7 +101,15 @@ public function test_censored_words()
 
 	public function test_highlight_code()
 	{
-		$expect = "\n<?php var_dump(\$this); ?> \n\n";
+		// PHP 8.4 changed highlight_string() output format
+		if (PHP_VERSION_ID >= 80400)
+		{
+			$expect = "
<?php var_dump(\$this); ?> ?>
"; + } + else + { + $expect = "\n<?php var_dump(\$this); ?> \n\n"; + } $this->assertEquals($expect, highlight_code('')); } From e486ffe9ada89df4befcc7f9a8c06970a8264f37 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 08:18:55 -0700 Subject: [PATCH 33/68] fix: handle null language line key in Lang::line() for PHP 8.5+. --- system/core/Lang.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/core/Lang.php b/system/core/Lang.php index 18299060c09..95cc8039c46 100644 --- a/system/core/Lang.php +++ b/system/core/Lang.php @@ -190,7 +190,7 @@ public function load($langfile, $idiom = '', $return = FALSE, $add_suffix = TRUE */ public function line($line, $log_errors = TRUE) { - $value = isset($this->language[$line]) ? $this->language[$line] : FALSE; + $value = ($line !== NULL && isset($this->language[$line])) ? $this->language[$line] : FALSE; // Because killer robots like unicorns! if ($value === FALSE && $log_errors === TRUE) From 5300de430869eec6a3a5f555a0bd68785f39be37 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 08:20:21 -0700 Subject: [PATCH 34/68] fix(image): add PHP 8.0+ compatibility for imagedestroy() deprecation. imagedestroy() has been a no-op since PHP 8.0 when GD images changed from resources to GdImage objects with automatic garbage collection. Closes #35. --- system/helpers/captcha_helper.php | 5 ++++- system/libraries/Image_lib.php | 31 +++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/system/helpers/captcha_helper.php b/system/helpers/captcha_helper.php index e7e760a5f81..2bee0082efc 100644 --- a/system/helpers/captcha_helper.php +++ b/system/helpers/captcha_helper.php @@ -375,7 +375,10 @@ function create_captcha($data) $img_class = (bool) strlen($img_class) ? 'class="'.$img_class.'" ' : ''; $img = ''; - ImageDestroy($im); + if (PHP_VERSION_ID < 80000) + { + imagedestroy($im); + } return array('word' => $word, 'time' => $now, 'image' => $img, 'filename' => $img_filename); } diff --git a/system/libraries/Image_lib.php b/system/libraries/Image_lib.php index 9067eff692f..e933d38bf89 100644 --- a/system/libraries/Image_lib.php +++ b/system/libraries/Image_lib.php @@ -841,8 +841,11 @@ public function image_process_gd($action = 'resize') } // Kill the file handles - imagedestroy($dst_img); - imagedestroy($src_img); + if (PHP_VERSION_ID < 80000) + { + imagedestroy($dst_img); + imagedestroy($src_img); + } if ($this->dynamic_output !== TRUE) { @@ -1050,8 +1053,11 @@ public function image_rotate_gd() } // Kill the file handles - imagedestroy($dst_img); - imagedestroy($src_img); + if (PHP_VERSION_ID < 80000) + { + imagedestroy($dst_img); + imagedestroy($src_img); + } chmod($this->full_dst_path, $this->file_permissions); @@ -1129,7 +1135,10 @@ public function image_mirror_gd() } // Kill the file handles - imagedestroy($src_img); + if (PHP_VERSION_ID < 80000) + { + imagedestroy($src_img); + } chmod($this->full_dst_path, $this->file_permissions); @@ -1259,8 +1268,11 @@ public function overlay_watermark() return FALSE; } - imagedestroy($src_img); - imagedestroy($wm_img); + if (PHP_VERSION_ID < 80000) + { + imagedestroy($src_img); + imagedestroy($wm_img); + } return TRUE; } @@ -1430,7 +1442,10 @@ public function text_watermark() $this->image_save_gd($src_img); } - imagedestroy($src_img); + if (PHP_VERSION_ID < 80000) + { + imagedestroy($src_img); + } return TRUE; } From 5276118ce03b4342daab70531729fae2732252eb Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 08:23:28 -0700 Subject: [PATCH 35/68] fix(tests): conditionally call setAccessible() for PHP 8.1+ compatibility. ReflectionMethod/Property::setAccessible() is deprecated in PHP 8.5 and has had no effect since PHP 8.1. --- tests/codeigniter/core/Input_test.php | 10 +++++++-- tests/codeigniter/core/Log_test.php | 25 ++++++++++++++------- tests/codeigniter/libraries/Upload_test.php | 5 ++++- tests/codeigniter/libraries/Xmlrpc_test.php | 5 ++++- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/tests/codeigniter/core/Input_test.php b/tests/codeigniter/core/Input_test.php index 93d1b7118c3..e1ffbe05e40 100644 --- a/tests/codeigniter/core/Input_test.php +++ b/tests/codeigniter/core/Input_test.php @@ -161,7 +161,10 @@ public function test_server() public function test_fetch_from_array() { $reflection = new ReflectionMethod($this->input, '_fetch_from_array'); - $reflection->setAccessible(TRUE); + if (PHP_VERSION_ID < 80100) + { + $reflection->setAccessible(TRUE); + } $data = array( 'foo' => 'bar', @@ -259,7 +262,10 @@ public function test_get_request_header() public function test_ip_address() { $reflection = new ReflectionProperty($this->input, 'ip_address'); - $reflection->setAccessible(TRUE); + if (PHP_VERSION_ID < 80100) + { + $reflection->setAccessible(TRUE); + } $reflection->setValue($this->input, '127.0.0.1'); $this->assertEquals('127.0.0.1', $this->input->ip_address()); diff --git a/tests/codeigniter/core/Log_test.php b/tests/codeigniter/core/Log_test.php index 3f0717b7920..a9f2dedb213 100644 --- a/tests/codeigniter/core/Log_test.php +++ b/tests/codeigniter/core/Log_test.php @@ -4,17 +4,20 @@ class Log_test extends CI_TestCase { public function test_configuration() { $path = new ReflectionProperty('CI_Log', '_log_path'); - $path->setAccessible(TRUE); $threshold = new ReflectionProperty('CI_Log', '_threshold'); - $threshold->setAccessible(TRUE); $date_fmt = new ReflectionProperty('CI_Log', '_date_fmt'); - $date_fmt->setAccessible(TRUE); $filename = new ReflectionProperty('CI_Log', '_log_filename'); - $filename->setAccessible(TRUE); $file_perms = new ReflectionProperty('CI_Log', '_file_permissions'); - $file_perms->setAccessible(TRUE); $enabled = new ReflectionProperty('CI_Log', '_enabled'); - $enabled->setAccessible(TRUE); + if (PHP_VERSION_ID < 80100) + { + $path->setAccessible(TRUE); + $threshold->setAccessible(TRUE); + $date_fmt->setAccessible(TRUE); + $filename->setAccessible(TRUE); + $file_perms->setAccessible(TRUE); + $enabled->setAccessible(TRUE); + } $this->ci_set_config('log_path', $this->ci_readonly_dir->url()); $this->ci_set_config('log_threshold', 'z'); @@ -48,7 +51,10 @@ public function test_configuration() public function test_log_file_extension_backward_compatibility() { $filename = new ReflectionProperty('CI_Log', '_log_filename'); - $filename->setAccessible(TRUE); + if (PHP_VERSION_ID < 80100) + { + $filename->setAccessible(TRUE); + } $this->ci_set_config('log_path', ''); $this->ci_set_config('log_threshold', 0); @@ -77,7 +83,10 @@ public function test_format_line() $instance = new CI_Log(); $format_line = new ReflectionMethod($instance, '_format_line'); - $format_line->setAccessible(TRUE); + if (PHP_VERSION_ID < 80100) + { + $format_line->setAccessible(TRUE); + } $this->assertEquals( $format_line->invoke($instance, 'LEVEL', 'Timestamp', 'Message'), "LEVEL - Timestamp --> Message".PHP_EOL diff --git a/tests/codeigniter/libraries/Upload_test.php b/tests/codeigniter/libraries/Upload_test.php index b660d0ec85f..0d069d9a9d0 100644 --- a/tests/codeigniter/libraries/Upload_test.php +++ b/tests/codeigniter/libraries/Upload_test.php @@ -27,7 +27,10 @@ public function test___construct_initialize() $reflection = new ReflectionClass($upload); $reflection = $reflection->getProperty('_file_name_override'); - $reflection->setAccessible(TRUE); + if (PHP_VERSION_ID < 80100) + { + $reflection->setAccessible(TRUE); + } $this->assertEquals('foo', $reflection->getValue($upload)); $this->assertTrue($upload->file_ext_tolower); diff --git a/tests/codeigniter/libraries/Xmlrpc_test.php b/tests/codeigniter/libraries/Xmlrpc_test.php index 46d73bf6e3e..e767585f843 100644 --- a/tests/codeigniter/libraries/Xmlrpc_test.php +++ b/tests/codeigniter/libraries/Xmlrpc_test.php @@ -12,7 +12,10 @@ public function set_up() $this->input = new CI_Input($security); $this->input_lib_raw_stream = new ReflectionProperty($this->input, '_raw_input_stream'); - $this->input_lib_raw_stream->setAccessible(TRUE); + if (PHP_VERSION_ID < 80100) + { + $this->input_lib_raw_stream->setAccessible(TRUE); + } $this->ci_instance_var('input', $this->input); $this->ci_instance_var('security', $security); From b245092521139df2fb7a5043b574ea2419a147b2 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 08:28:58 -0700 Subject: [PATCH 36/68] fix(tests): add PHPUnit compatibility shim for assertObjectHasProperty. assertObjectHasAttribute() is deprecated in PHPUnit 9.6+. Add assertObjectHasPropertyShim() helper that uses the appropriate method. --- tests/codeigniter/core/Loader_test.php | 28 ++++++++++----------- tests/codeigniter/libraries/Driver_test.php | 8 +++--- tests/mocks/ci_testcase.php | 20 +++++++++++++++ 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/tests/codeigniter/core/Loader_test.php b/tests/codeigniter/core/Loader_test.php index b14b7178019..6482765d0b9 100644 --- a/tests/codeigniter/core/Loader_test.php +++ b/tests/codeigniter/core/Loader_test.php @@ -36,7 +36,7 @@ public function test_library() // Test loading as an array. $this->assertInstanceOf('CI_Loader', $this->load->library(array($lib))); $this->assertTrue(class_exists($class), $class.' does not exist'); - $this->assertObjectHasAttribute($lib, $this->ci_obj); + $this->assertObjectHasPropertyShim($lib, $this->ci_obj); $this->assertInstanceOf($class, $this->ci_obj->$lib); // Create library in VFS @@ -88,21 +88,21 @@ public function test_library_extension() $this->assertInstanceOf('CI_Loader', $this->load->library($lib)); $this->assertTrue(class_exists($class), $class.' does not exist'); $this->assertTrue(class_exists($ext), $ext.' does not exist'); - $this->assertObjectHasAttribute($name, $this->ci_obj); + $this->assertObjectHasPropertyShim($name, $this->ci_obj); $this->assertInstanceOf($class, $this->ci_obj->$name); $this->assertInstanceOf($ext, $this->ci_obj->$name); // Test reloading with object name $obj = 'exttest'; $this->assertInstanceOf('CI_Loader', $this->load->library($lib, NULL, $obj)); - $this->assertObjectHasAttribute($obj, $this->ci_obj); + $this->assertObjectHasPropertyShim($obj, $this->ci_obj); $this->assertInstanceOf($class, $this->ci_obj->$obj); $this->assertInstanceOf($ext, $this->ci_obj->$obj); // Test reloading unset($this->ci_obj->$name); $this->assertInstanceOf('CI_Loader', $this->load->library($lib)); - $this->assertObjectHasAttribute($name, $this->ci_obj); + $this->assertObjectHasPropertyShim($name, $this->ci_obj); // Create baseless library $name = 'ext_baseless_lib'; @@ -140,7 +140,7 @@ public function test_library_config() $obj = 'testy'; $this->assertInstanceOf('CI_Loader', $this->load->library($lib, NULL, $obj)); $this->assertTrue(class_exists($class), $class.' does not exist'); - $this->assertObjectHasAttribute($obj, $this->ci_obj); + $this->assertObjectHasPropertyShim($obj, $this->ci_obj); $this->assertInstanceOf($class, $this->ci_obj->$obj); $this->assertEquals($cfg, $this->ci_obj->$obj->config); @@ -172,7 +172,7 @@ public function test_load_library_in_application_dir() // Was the model class instantiated. $this->assertTrue(class_exists($class), $class.' does not exist'); - $this->assertObjectHasAttribute($lib, $this->ci_obj); + $this->assertObjectHasPropertyShim($lib, $this->ci_obj); $this->assertInstanceOf($class, $this->ci_obj->$lib); } @@ -193,13 +193,13 @@ class_exists('CI_Driver_Library', TRUE); // Test loading as an array. $this->assertInstanceOf('CI_Loader', $this->load->driver(array($driver))); $this->assertTrue(class_exists($class), $class.' does not exist'); - $this->assertObjectHasAttribute($driver, $this->ci_obj); + $this->assertObjectHasPropertyShim($driver, $this->ci_obj); $this->assertInstanceOf($class, $this->ci_obj->$driver); // Test loading as a library with a name $obj = 'testdrive'; $this->assertInstanceOf('CI_Loader', $this->load->library($driver, NULL, $obj)); - $this->assertObjectHasAttribute($obj, $this->ci_obj); + $this->assertObjectHasPropertyShim($obj, $this->ci_obj); $this->assertInstanceOf($class, $this->ci_obj->$obj); // Test a string given to params @@ -222,7 +222,7 @@ public function test_models() // Was the model class instantiated. $this->assertTrue(class_exists($model)); - $this->assertObjectHasAttribute($model, $this->ci_obj); + $this->assertObjectHasPropertyShim($model, $this->ci_obj); // Test no model given $this->assertInstanceOf('CI_Loader', $this->load->model('')); @@ -248,8 +248,8 @@ public function test_model_subdir() // Was the model class instantiated? $this->assertTrue(class_exists($model)); - $this->assertObjectHasAttribute($name, $this->ci_obj); - $this->assertObjectHasAttribute($name, $this->ci_obj); + $this->assertObjectHasPropertyShim($name, $this->ci_obj); + $this->assertObjectHasPropertyShim($name, $this->ci_obj); $this->assertInstanceOf($base, $this->ci_obj->$name); $this->assertInstanceOf($model, $this->ci_obj->$name); @@ -607,17 +607,17 @@ public function test_initialize() // Verify library $this->assertTrue(class_exists($lib_class), $lib_class.' does not exist'); - $this->assertObjectHasAttribute($lib, $this->ci_obj); + $this->assertObjectHasPropertyShim($lib, $this->ci_obj); $this->assertInstanceOf($lib_class, $this->ci_obj->$lib); // Verify driver $this->assertTrue(class_exists($drv_class), $drv_class.' does not exist'); - $this->assertObjectHasAttribute($drv, $this->ci_obj); + $this->assertObjectHasPropertyShim($drv, $this->ci_obj); $this->assertInstanceOf($drv_class, $this->ci_obj->$drv); // Verify model $this->assertTrue(class_exists($model), $model.' does not exist'); - $this->assertObjectHasAttribute($model, $this->ci_obj); + $this->assertObjectHasPropertyShim($model, $this->ci_obj); $this->assertInstanceOf($model, $this->ci_obj->$model); // Verify config calls diff --git a/tests/codeigniter/libraries/Driver_test.php b/tests/codeigniter/libraries/Driver_test.php index ea5cfa235f8..f32080ae592 100644 --- a/tests/codeigniter/libraries/Driver_test.php +++ b/tests/codeigniter/libraries/Driver_test.php @@ -51,12 +51,12 @@ public function test_load_driver() $this->assertEquals($this->name, $this->lib->get_name()); // Was driver loaded? - $this->assertObjectHasAttribute($driver, $this->lib); + $this->assertObjectHasPropertyShim($driver, $this->lib); $this->assertInstanceOf($class, $this->lib->$driver); $this->assertInstanceOf('CI_Driver', $this->lib->$driver); // Was decorate called? - $this->assertObjectHasAttribute($prop, $this->lib->$driver); + $this->assertObjectHasPropertyShim($prop, $this->lib->$driver); $this->assertTrue($this->lib->$driver->$prop); // Do we get an error for an invalid driver? @@ -86,7 +86,7 @@ public function test_load_app_driver() $this->assertNotNull($this->lib->load_driver($driver)); // Was driver loaded? - $this->assertObjectHasAttribute($driver, $this->lib); + $this->assertObjectHasPropertyShim($driver, $this->lib); $this->assertInstanceOf($class, $this->lib->$driver); $this->assertInstanceOf('CI_Driver', $this->lib->$driver); @@ -120,7 +120,7 @@ public function test_load_driver_ext() $this->assertNotNull($this->lib->load_driver($driver)); // Was driver loaded? - $this->assertObjectHasAttribute($driver, $this->lib); + $this->assertObjectHasPropertyShim($driver, $this->lib); $this->assertInstanceOf($class, $this->lib->$driver); $this->assertInstanceOf($baseclass, $this->lib->$driver); $this->assertInstanceOf('CI_Driver', $this->lib->$driver); diff --git a/tests/mocks/ci_testcase.php b/tests/mocks/ci_testcase.php index e755bd0be5b..3f6de627bc7 100644 --- a/tests/mocks/ci_testcase.php +++ b/tests/mocks/ci_testcase.php @@ -398,4 +398,24 @@ public function setExpectedException($exception_class, $exception_message = '', parent::setExpectedException($exception_class, $exception_message, $exception_code); } } + + // -------------------------------------------------------------------- + + /** + * PHPUnit compatibility shim for assertObjectHasProperty + * + * assertObjectHasAttribute() is deprecated in PHPUnit 9.6+ and removed in PHPUnit 10. + * assertObjectHasProperty() was added in PHPUnit 9.6. + */ + public function assertObjectHasPropertyShim($propertyName, $object, $message = '') + { + if (method_exists($this, 'assertObjectHasProperty')) + { + $this->assertObjectHasProperty($propertyName, $object, $message); + } + else + { + $this->assertObjectHasAttribute($propertyName, $object, $message); + } + } } From 397a8b85651902e5ec967a8341630ea3bf9ac964 Mon Sep 17 00:00:00 2001 From: RedDragonWebDesign <43397938+RedDragonWebDesign@users.noreply.github.com> Date: Sat, 27 Dec 2025 07:31:11 -0800 Subject: [PATCH 37/68] mimes: add support for webp image type Why - the .webp image type has become common on the internet What - modify application/config/mimes.php to include `'webp' => 'image/webp'` --- application/config/mimes.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/config/mimes.php b/application/config/mimes.php index b2e989fea9e..d806f69246f 100644 --- a/application/config/mimes.php +++ b/application/config/mimes.php @@ -182,5 +182,6 @@ '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' + 'oth' => 'application/vnd.oasis.opendocument.text-web', + 'webp' => 'image/webp' ); From 3fea824ff94ebbe580e754470e4f8dae363d8767 Mon Sep 17 00:00:00 2001 From: Ashraf Kaabi Date: Sat, 27 Dec 2025 18:37:57 +0200 Subject: [PATCH 38/68] Update DB_query_builder.php * Update DB_query_builder.php Fix: cast trim() argument to string to prevent null deprecation in PHP 8.x * Refactor DB_query_builder for array initialization Updated array initialization and type handling in DB_query_builder. --- system/database/DB_query_builder.php | 32 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/system/database/DB_query_builder.php b/system/database/DB_query_builder.php index feb5f8cbee3..e3a4f16caf5 100644 --- a/system/database/DB_query_builder.php +++ b/system/database/DB_query_builder.php @@ -294,7 +294,7 @@ public function select($select = '*', $escape = NULL) foreach ($select as $val) { - $val = trim($val); + $val = trim((string)$val); if ($val !== '') { @@ -408,10 +408,10 @@ protected function _max_min_avg_sum($select = '', $alias = '', $type = 'MAX') if ($alias === '') { - $alias = $this->_create_alias_from_table(trim($select)); + $alias = $this->_create_alias_from_table(trim((string)$select)); } - $sql = $type.'('.$this->protect_identifiers(trim($select)).') AS '.$this->escape_identifiers(trim($alias)); + $sql = $type.'('.$this->protect_identifiers(trim((string)$select)).') AS '.$this->escape_identifiers(trim((string)$alias)); $this->qb_select[] = $sql; $this->qb_no_escape[] = NULL; @@ -478,7 +478,7 @@ public function from($from) { foreach (explode(',', $val) as $v) { - $v = trim($v); + $v = trim((string)$v); $this->_track_aliases($v); $this->qb_from[] = $v = $this->protect_identifiers($v, TRUE, NULL, FALSE); @@ -492,7 +492,7 @@ public function from($from) } else { - $val = trim($val); + $val = trim((string)$val); // Extract any aliases that might exist. We use this information // in the protect_identifiers to know whether to add a table prefix @@ -534,7 +534,7 @@ public function join($table, $cond, $type = '', $escape = NULL) throw new InvalidArgumentException('join() expects parameter 3 to be a string, ' . gettype($type) . ' given'); } - $type = trim(strtoupper($type).' JOIN'); + $type = trim((string)strtoupper($type).' JOIN'); preg_match('#^(NATURAL\s+)?((LEFT|RIGHT|FULL)\s+)?((INNER|OUTER)\s+)?JOIN$#', $type) OR $type = 'JOIN'; // Extract any aliases that might exist. We use this information @@ -1217,7 +1217,7 @@ public function group_by($by, $escape = NULL) foreach ($by as $val) { - $val = trim($val); + $val = trim((string)$val); if ($val !== '') { @@ -1289,7 +1289,7 @@ public function order_by($orderby, $direction = '', $escape = NULL) throw new InvalidArgumentException('order_by() expects parameter 2 to be a string, ' . gettype($direction) . ' given'); } - $direction = strtoupper(trim($direction)); + $direction = strtoupper(trim((string)$direction)); if ($direction === 'RANDOM') { @@ -1320,9 +1320,9 @@ public function order_by($orderby, $direction = '', $escape = NULL) $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); + $qb_orderby[] = ($direction === '' && preg_match('/\s+(ASC|DESC)$/i', rtrim((string)$field), $match, PREG_OFFSET_CAPTURE)) + ? array('field' => ltrim((string)substr($field, 0, $match[0][1])), 'direction' => ' '.$match[1][0], 'escape' => TRUE) + : array('field' => trim((string)$field), 'direction' => $direction, 'escape' => TRUE); } } @@ -2377,7 +2377,7 @@ protected function _track_aliases($table) $table = preg_replace('/\s+AS\s+/i', ' ', $table); // Grab the alias - $table = trim(strrchr($table, ' ')); + $table = trim((string)strrchr($table, ' ')); // Store the alias, if it doesn't already exist if ( ! in_array($table, $this->qb_aliased_tables, TRUE)) @@ -2520,12 +2520,12 @@ protected function _compile_wh($qb_key) if ( ! empty($matches[4])) { - $this->_is_literal($matches[4]) OR $matches[4] = $this->protect_identifiers(trim($matches[4])); + $this->_is_literal($matches[4]) OR $matches[4] = $this->protect_identifiers(trim((string)$matches[4])); $matches[4] = ' '.$matches[4]; } - $conditions[$ci] = $matches[1].$this->protect_identifiers(trim($matches[2])) - .' '.trim($matches[3]).$matches[4].$matches[5]; + $conditions[$ci] = $matches[1].$this->protect_identifiers(trim((string)$matches[2])) + .' '.trim((string)$matches[3]).$matches[4].$matches[5]; } $this->{$qb_key}[$i] = implode('', $conditions).(isset($this->{$qb_key}[$i]['value']) ? ' '.$this->{$qb_key}[$i]['value'] : ''); @@ -2796,7 +2796,7 @@ protected function _merge_cache() */ protected function _is_literal($str) { - $str = trim($str); + $str = trim((string)$str); if (empty($str) OR ctype_digit($str) OR (string) (float) $str === $str OR in_array(strtoupper($str), array('TRUE', 'FALSE'), TRUE)) { From d7ccac0d254b5aed7fee40d90152b974142d15d8 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 10:27:48 -0700 Subject: [PATCH 39/68] fix(xmlrpc): Replace deprecated xml_set_object() + string callback pattern with callable arrays [$object, 'method'], which has always been supported. Closes #18. --- system/libraries/Xmlrpc.php | 5 ++--- system/libraries/Xmlrpcs.php | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/system/libraries/Xmlrpc.php b/system/libraries/Xmlrpc.php index 5238c056f9a..be4e34497c7 100644 --- a/system/libraries/Xmlrpc.php +++ b/system/libraries/Xmlrpc.php @@ -1160,10 +1160,9 @@ public function parseResponse($fp) '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_element_handler($parser, [$this, 'open_tag'], [$this, 'closing_tag']); + xml_set_character_data_handler($parser, [$this, 'character_data']); //xml_set_default_handler($parser, 'default_handler'); // Get headers diff --git a/system/libraries/Xmlrpcs.php b/system/libraries/Xmlrpcs.php index 481938b3400..557c9912ba4 100644 --- a/system/libraries/Xmlrpcs.php +++ b/system/libraries/Xmlrpcs.php @@ -244,10 +244,9 @@ public function parseRequest($data = '') '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_element_handler($parser, [$parser_object, 'open_tag'], [$parser_object, 'closing_tag']); + xml_set_character_data_handler($parser, [$parser_object, 'character_data']); //xml_set_default_handler($parser, 'default_handler'); //------------------------------------- From 746767e238ac79cda163a7940d6536082b25c1bd Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 10:43:34 -0700 Subject: [PATCH 40/68] fix(ci): replace deprecated set-output with $GITHUB_OUTPUT. --- .github/workflows/test-phpunit.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-phpunit.yml b/.github/workflows/test-phpunit.yml index 22fcee6c761..c4f1605bc52 100644 --- a/.github/workflows/test-phpunit.yml +++ b/.github/workflows/test-phpunit.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.5', 8.4', '8.3', '8.2', '8.1', '8.0', '7.4', '7.3', '7.2', '7.1', '7.0', '5.6', '5.5', '5.4' ] + php: [ '8.5', '8.4', '8.3', '8.2', '8.1', '8.0', '7.4', '7.3', '7.2', '7.1', '7.0', '5.6', '5.5', '5.4' ] DB: [ 'pdo/mysql', 'pdo/pgsql', 'pdo/sqlite', 'mysqli', 'pgsql', 'sqlite' ] compiler: [ default ] include: @@ -174,7 +174,7 @@ jobs: - name: Get composer cache directory id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer dependencies uses: actions/cache@v4 with: From fa7e9fe31e03ad79a8229e6e730879933eb8c523 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 10:46:56 -0700 Subject: [PATCH 41/68] fix(tests): correct PHP version check for highlight_string() format change. The highlight_string() output format changed in PHP 8.3, not 8.4. --- tests/codeigniter/helpers/text_helper_test.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/codeigniter/helpers/text_helper_test.php b/tests/codeigniter/helpers/text_helper_test.php index 513f28af197..43024abdf32 100644 --- a/tests/codeigniter/helpers/text_helper_test.php +++ b/tests/codeigniter/helpers/text_helper_test.php @@ -101,8 +101,8 @@ public function test_censored_words() public function test_highlight_code() { - // PHP 8.4 changed highlight_string() output format - if (PHP_VERSION_ID >= 80400) + // PHP 8.3 changed highlight_string() output format + if (PHP_VERSION_ID >= 80300) { $expect = "
<?php var_dump(\$this); ?> ?>
"; } From b7a984dd1d1c17b70756ccf75c8729501d39b055 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 10:59:01 -0700 Subject: [PATCH 42/68] docs: add badges for PHPUnit tests, PHP compatibility, Packagist version, and downloads --- readme.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/readme.rst b/readme.rst index 10ba1d89d20..2f902a99477 100644 --- a/readme.rst +++ b/readme.rst @@ -2,6 +2,20 @@ What is this repository? ######################## +.. image:: https://github.com/pocketarc/codeigniter/actions/workflows/test-phpunit.yml/badge.svg?branch=develop + :target: https://github.com/pocketarc/codeigniter/actions/workflows/test-phpunit.yml + :alt: PHPUnit Tests + +.. image:: https://img.shields.io/badge/PHP-5.4%20--%208.5-8892BF?logo=php + :alt: PHP 5.4 - 8.5 + +.. image:: https://img.shields.io/packagist/v/pocketarc/codeigniter + :target: https://packagist.org/packages/pocketarc/codeigniter + :alt: Packagist Version + +.. image:: https://img.shields.io/packagist/dt/pocketarc/codeigniter + :alt: Packagist Downloads + This is a fork of CodeIgniter 3, with the goal of keeping it up to date with modern PHP versions. There is no intention to add new features or change the way CI3 works. This is purely a maintenance fork. **PHP Compatibility:** From 5466331f18be1217907c01c5bc3621e95b802c91 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 11:00:07 -0700 Subject: [PATCH 43/68] docs: inline images --- readme.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/readme.rst b/readme.rst index 2f902a99477..639d7ddf28d 100644 --- a/readme.rst +++ b/readme.rst @@ -2,18 +2,20 @@ What is this repository? ######################## -.. image:: https://github.com/pocketarc/codeigniter/actions/workflows/test-phpunit.yml/badge.svg?branch=develop +|tests| |php| |version| |downloads| + +.. |tests| image:: https://github.com/pocketarc/codeigniter/actions/workflows/test-phpunit.yml/badge.svg?branch=develop :target: https://github.com/pocketarc/codeigniter/actions/workflows/test-phpunit.yml :alt: PHPUnit Tests -.. image:: https://img.shields.io/badge/PHP-5.4%20--%208.5-8892BF?logo=php +.. |php| image:: https://img.shields.io/badge/PHP-5.4%20--%208.5-8892BF?logo=php :alt: PHP 5.4 - 8.5 -.. image:: https://img.shields.io/packagist/v/pocketarc/codeigniter +.. |version| image:: https://img.shields.io/packagist/v/pocketarc/codeigniter :target: https://packagist.org/packages/pocketarc/codeigniter :alt: Packagist Version -.. image:: https://img.shields.io/packagist/dt/pocketarc/codeigniter +.. |downloads| image:: https://img.shields.io/packagist/dt/pocketarc/codeigniter :alt: Packagist Downloads This is a fork of CodeIgniter 3, with the goal of keeping it up to date with modern PHP versions. There is no intention to add new features or change the way CI3 works. This is purely a maintenance fork. From eb3ac6b68780bb317664bdab2423b1da63510dff Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 11:05:54 -0700 Subject: [PATCH 44/68] docs: update project description for Packagist. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e7360a93de7..7a7f02e8ecd 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "description": "The CodeIgniter framework", + "description": "Actively maintained CodeIgniter 3 fork with support for PHP 5.4 all the way PHP 8.5. Fully backward compatible.", "name": "pocketarc/codeigniter", "type": "project", "homepage": "https://github.com/pocketarc/codeigniter", From 1d7cc44a0d5839bc7fcdc8b7c3efba516498d4fb Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sat, 27 Dec 2025 11:07:40 -0700 Subject: [PATCH 45/68] docs: update project description for Packagist. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7a7f02e8ecd..fc6e74b2857 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "description": "Actively maintained CodeIgniter 3 fork with support for PHP 5.4 all the way PHP 8.5. Fully backward compatible.", + "description": "Actively maintained CodeIgniter 3 fork with support for PHP 5.4 - PHP 8.5 (and beyond). Fully backward compatible.", "name": "pocketarc/codeigniter", "type": "project", "homepage": "https://github.com/pocketarc/codeigniter", From f534bc3a0fe76ef98ac7a7a85d7e31cdd1940e4c Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Mon, 19 Jan 2026 07:20:01 -0700 Subject: [PATCH 46/68] feat: restore Cart library removed in CI 3.2. --- system/libraries/Cart.php | 568 ++++++++++++++++++ tests/mocks/autoloader.php | 1 + .../source/installation/upgrade_320.rst | 1 - user_guide_src/source/libraries/cart.rst | 398 ++++++++++++ 4 files changed, 967 insertions(+), 1 deletion(-) create mode 100644 system/libraries/Cart.php create mode 100644 user_guide_src/source/libraries/cart.rst diff --git a/system/libraries/Cart.php b/system/libraries/Cart.php new file mode 100644 index 00000000000..f8244b15377 --- /dev/null +++ b/system/libraries/Cart.php @@ -0,0 +1,568 @@ +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/tests/mocks/autoloader.php b/tests/mocks/autoloader.php index ce3454dc9ed..3e6b0b60432 100644 --- a/tests/mocks/autoloader.php +++ b/tests/mocks/autoloader.php @@ -33,6 +33,7 @@ function autoload($class) $ci_libraries = array( 'Calendar', + 'Cart', 'Driver_Library', 'Email', 'Encrypt', diff --git a/user_guide_src/source/installation/upgrade_320.rst b/user_guide_src/source/installation/upgrade_320.rst index 6308fa7fa47..f047d50d87c 100644 --- a/user_guide_src/source/installation/upgrade_320.rst +++ b/user_guide_src/source/installation/upgrade_320.rst @@ -223,7 +223,6 @@ CodeIgniter versions that have been removed in 3.2.0: - ``form_prep()`` :doc:`Form Helper <../helpers/form_helper>` function (use :php:func:`html_escape()` instead) - The entire *Encrypt Library* (the newer :doc:`Encryption Library <../libraries/encryption>` is still available) -- The entire *Cart Library* (an archived version is available on GitHub: `bcit-ci/ci3-cart-library `_) - The entire *Javascript Library* (it was always experimental in the first place) - The entire *Email Helper*, which only had two functions: diff --git a/user_guide_src/source/libraries/cart.rst b/user_guide_src/source/libraries/cart.rst new file mode 100644 index 00000000000..be343320db8 --- /dev/null +++ b/user_guide_src/source/libraries/cart.rst @@ -0,0 +1,398 @@ +################### +Shopping Cart Class +################### + +The Cart Class permits items to be added to a session that stays active +while a user is browsing your site. These items can be retrieved and +displayed in a standard "shopping cart" format, allowing the user to +update the quantity or remove items from the cart. + +.. important:: The Cart library is DEPRECATED and should not be used. + It is currently only kept for backwards compatibility. + +Please note that the Cart Class ONLY provides the core "cart" +functionality. It does not provide shipping, credit card authorization, +or other processing components. + +.. contents:: + :local: + +.. raw:: html + +
+ +******************** +Using the Cart Class +******************** + +Initializing the Shopping Cart Class +==================================== + +.. important:: The Cart class utilizes CodeIgniter's :doc:`Session + Class ` to save the cart information to a database, so + before using the Cart class you must set up a database table as + indicated in the :doc:`Session Documentation `, and set the + session preferences in your application/config/config.php file to + utilize a database. + +To initialize the Shopping Cart Class in your controller constructor, +use the ``$this->load->library()`` method:: + + $this->load->library('cart'); + +Once loaded, the Cart object will be available using:: + + $this->cart + +.. note:: The Cart Class will load and initialize the Session Class + automatically, so unless you are using sessions elsewhere in your + application, you do not need to load the Session class. + +Adding an Item to The Cart +========================== + +To add an item to the shopping cart, simply pass an array with the +product information to the ``$this->cart->insert()`` method, as shown +below:: + + $data = array( + 'id' => 'sku_123ABC', + 'qty' => 1, + 'price' => 39.95, + 'name' => 'T-Shirt', + 'options' => array('Size' => 'L', 'Color' => 'Red') + ); + + $this->cart->insert($data); + +.. important:: The first four array indexes above (id, qty, price, and + name) are **required**. If you omit any of them the data will not be + saved to the cart. The fifth index (options) is optional. It is intended + to be used in cases where your product has options associated with it. + Use an array for options, as shown above. + +The five reserved indexes are: + +- **id** - Each product in your store must have a unique identifier. + Typically this will be an "sku" or other such identifier. +- **qty** - The quantity being purchased. +- **price** - The price of the item. +- **name** - The name of the item. +- **options** - Any additional attributes that are needed to identify + the product. These must be passed via an array. + +In addition to the five indexes above, there are two reserved words: +rowid and subtotal. These are used internally by the Cart class, so +please do NOT use those words as index names when inserting data into +the cart. + +Your array may contain additional data. Anything you include in your +array will be stored in the session. However, it is best to standardize +your data among all your products in order to make displaying the +information in a table easier. + +:: + + $data = array( + 'id' => 'sku_123ABC', + 'qty' => 1, + 'price' => 39.95, + 'name' => 'T-Shirt', + 'coupon' => 'XMAS-50OFF' + ); + + $this->cart->insert($data); + +The ``insert()`` method will return the $rowid if you successfully insert a +single item. + +Adding Multiple Items to The Cart +================================= + +By using a multi-dimensional array, as shown below, it is possible to +add multiple products to the cart in one action. This is useful in cases +where you wish to allow people to select from among several items on the +same page. + +:: + + $data = array( + array( + 'id' => 'sku_123ABC', + 'qty' => 1, + 'price' => 39.95, + 'name' => 'T-Shirt', + 'options' => array('Size' => 'L', 'Color' => 'Red') + ), + array( + 'id' => 'sku_567ZYX', + 'qty' => 1, + 'price' => 9.95, + 'name' => 'Coffee Mug' + ), + array( + 'id' => 'sku_965QRS', + 'qty' => 1, + 'price' => 29.95, + 'name' => 'Shot Glass' + ) + ); + + $this->cart->insert($data); + +Displaying the Cart +=================== + +To display the cart you will create a :doc:`view +file ` with code similar to the one shown below. + +Please note that this example uses the :doc:`form +helper `. + +:: + + + + + + + + + + + + + + + cart->contents() as $items): ?> + + + + + + + + + + + + + + + + + + + + +
QTYItem DescriptionItem PriceSub-Total
$i.'[qty]', 'value' => $items['qty'], 'maxlength' => '3', 'size' => '5')); ?> + + + cart->has_options($items['rowid']) == TRUE): ?> + +

+ cart->product_options($items['rowid']) as $option_name => $option_value): ?> + + :
+ + +

+ + + +
cart->format_number($items['price']); ?>$cart->format_number($items['subtotal']); ?>
 Total$cart->format_number($this->cart->total()); ?>
+ +

+ +Updating The Cart +================= + +To update the information in your cart, you must pass an array +containing the Row ID and one or more pre-defined properties to the +``$this->cart->update()`` method. + +.. note:: If the quantity is set to zero, the item will be removed from + the cart. + +:: + + $data = array( + 'rowid' => 'b99ccdf16028f015540f341130b6d8ec', + 'qty' => 3 + ); + + $this->cart->update($data); + + // Or a multi-dimensional array + + $data = array( + array( + 'rowid' => 'b99ccdf16028f015540f341130b6d8ec', + 'qty' => 3 + ), + array( + 'rowid' => 'xw82g9q3r495893iajdh473990rikw23', + 'qty' => 4 + ), + array( + 'rowid' => 'fh4kdkkkaoe30njgoe92rkdkkobec333', + 'qty' => 2 + ) + ); + + $this->cart->update($data); + +You may also update any property you have previously defined when +inserting the item such as options, price or other custom fields. + +:: + + $data = array( + 'rowid' => 'b99ccdf16028f015540f341130b6d8ec', + 'qty' => 1, + 'price' => 49.95, + 'coupon' => NULL + ); + + $this->cart->update($data); + +What is a Row ID? +***************** + +The row ID is a unique identifier that is generated by the cart code +when an item is added to the cart. The reason a unique ID is created +is so that identical products with different options can be managed +by the cart. + +For example, let's say someone buys two identical t-shirts (same product +ID), but in different sizes. The product ID (and other attributes) will +be identical for both sizes because it's the same shirt. The only +difference will be the size. The cart must therefore have a means of +identifying this difference so that the two sizes of shirts can be +managed independently. It does so by creating a unique "row ID" based on +the product ID and any options associated with it. + +In nearly all cases, updating the cart will be something the user does +via the "view cart" page, so as a developer, it is unlikely that you +will ever have to concern yourself with the "row ID", other than making +sure your "view cart" page contains this information in a hidden form +field, and making sure it gets passed to the ``update()`` method when +the update form is submitted. Please examine the construction of the +"view cart" page above for more information. + + +*************** +Class Reference +*************** + +.. php:class:: CI_Cart + + .. attribute:: $product_id_rules = '\.a-z0-9_-' + + These are the regular expression rules that we use to validate the product + ID - alpha-numeric, dashes, underscores, or periods by default + + .. attribute:: $product_name_rules = '\w \-\.\:' + + These are the regular expression rules that we use to validate the product ID and product name - alpha-numeric, dashes, underscores, colons or periods by + default + + .. attribute:: $product_name_safe = TRUE + + Whether or not to only allow safe product names. Default TRUE. + + + .. php:method:: insert([$items = array()]) + + :param array $items: Items to insert into the cart + :returns: TRUE on success, FALSE on failure + :rtype: bool + + Insert items into the cart and save it to the session table. Returns TRUE + on success and FALSE on failure. + + + .. php:method:: update([$items = array()]) + + :param array $items: Items to update in the cart + :returns: TRUE on success, FALSE on failure + :rtype: bool + + This method permits changing the properties of a given item. + 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 + for each item. + + .. php:method:: remove($rowid) + + :param int $rowid: ID of the item to remove from the cart + :returns: TRUE on success, FALSE on failure + :rtype: bool + + Allows you to remove an item from the shopping cart by passing it the + ``$rowid``. + + .. php:method:: total() + + :returns: Total amount + :rtype: int + + Displays the total amount in the cart. + + + .. php:method:: total_items() + + :returns: Total amount of items in the cart + :rtype: int + + Displays the total number of items in the cart. + + + .. php:method:: contents([$newest_first = FALSE]) + + :param bool $newest_first: Whether to order the array with newest items first + :returns: An array of cart contents + :rtype: array + + Returns an array containing everything in the cart. You can sort the + order by which the array is returned by passing it TRUE where the contents + will be sorted from newest to oldest, otherwise it is sorted from oldest + to newest. + + .. php:method:: get_item($row_id) + + :param int $row_id: Row ID to retrieve + :returns: Array of item data + :rtype: array + + Returns an array containing data for the item matching the specified row + ID, or FALSE if no such item exists. + + .. php:method:: has_options($row_id = '') + + :param int $row_id: Row ID to inspect + :returns: TRUE if options exist, FALSE otherwise + :rtype: bool + + Returns TRUE (boolean) if a particular row in the cart contains options. + This method is designed to be used in a loop with ``contents()``, since + you must pass the rowid to this method, as shown in the Displaying + the Cart example above. + + .. php:method:: product_options([$row_id = '']) + + :param int $row_id: Row ID + :returns: Array of product options + :rtype: array + + Returns an array of options for a particular product. This method is + designed to be used in a loop with ``contents()``, since you + must pass the rowid to this method, as shown in the Displaying the + Cart example above. + + .. php:method:: destroy() + + :rtype: void + + Permits you to destroy the cart. This method will likely be called + when you are finished processing the customer's order. \ No newline at end of file From 1aa03c7a4c893973f980ac47fbb9d5f5f7da1a02 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Mon, 19 Jan 2026 07:20:15 -0700 Subject: [PATCH 47/68] chore: Update version number. --- system/core/CodeIgniter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/core/CodeIgniter.php b/system/core/CodeIgniter.php index e52263e7032..9bb4de8db66 100644 --- a/system/core/CodeIgniter.php +++ b/system/core/CodeIgniter.php @@ -56,7 +56,7 @@ * @var string * */ - const CI_VERSION = '3.3.1'; + const CI_VERSION = '3.4.1'; /* * ------------------------------------------------------ From dca13487782dec544be1ffaa59493fa2121ee303 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Mon, 19 Jan 2026 07:46:06 -0700 Subject: [PATCH 48/68] docs: update upgrade guide and readme to reflect restored functionality for backward compatibility. --- readme.rst | 6 ++- .../source/installation/upgrade_320.rst | 50 +++---------------- 2 files changed, 11 insertions(+), 45 deletions(-) diff --git a/readme.rst b/readme.rst index 639d7ddf28d..e5b27d1d203 100644 --- a/readme.rst +++ b/readme.rst @@ -41,9 +41,11 @@ This fork commits to: - Maintaining compatibility with each new PHP version while still supporting PHP 5.4+ - Applying critical security fixes - Keeping changes minimal to preserve CI3 behavior -- Reverting unnecessary breaking changes in CodeIgniter 3.2.0-dev +- Reverting breaking changes in CodeIgniter 3.2.0-dev to maintain backward compatibility (e.g. restoring the Cart library, Email helper, and other deprecated-but-removed functionality) - Running the full CI3 test suite on PHP 8.2+ +If you find something that was removed in CI 3.2.0-dev and breaks backward compatibility for your application, please open an issue. We're happy to restore it. + This fork does NOT: - Add new features @@ -95,6 +97,8 @@ If you prefer the traditional approach of replacing the system directory: **Please review the upgrade guide:** `upgrade_320.rst `_ +Note: The upgrade guide has been updated to reflect functionality we've restored for backward compatibility (Cart library, Email helper, etc.). + Steps to upgrade: 1. Review the upgrade guide for breaking changes between 3.1.x and 3.2.0 diff --git a/user_guide_src/source/installation/upgrade_320.rst b/user_guide_src/source/installation/upgrade_320.rst index f047d50d87c..d622d60d243 100644 --- a/user_guide_src/source/installation/upgrade_320.rst +++ b/user_guide_src/source/installation/upgrade_320.rst @@ -24,45 +24,17 @@ may be at least runnable, we strongly discourage you from using any PHP versions the ones listed on the `PHP.net Supported Versions `_ page. -Step 3: Remove calls to ``CI_Model::__construct()`` -=================================================== +Step 3: Calls to ``CI_Model::__construct()`` (optional cleanup) +=============================================================== The class constructor for ``CI_Model`` never contained vital code or useful logic, only a single line to log a message. A change in CodeIgniter 3.1.7 moved this log message elsewhere and that naturally made the constructor -completely unnecessary. However, it was left in place to avoid immedate BC -breaks in a minor release. +completely unnecessary. -In version 3.2.0, that constructor is entirely removed, which would result -in fatal errors on attempts to call it. Particularly in code like this: -:: - - class Some_model extends CI_Model { - - public function __construct() - { - parent::__construct(); // calls CI_Model::__construct() - - do_some_other_thing(); - } - } - -All you need to do is remove that ``parent::__construct()`` call. On a side -note, the following seems to be a very common practice: -:: - - class Some_class extends CI_Something { - - public function __construct() - { - parent::__construct(); - } - } - -Please, do NOT do this! It's pointless; it serves no purpose and doesn't do -anything. If a parent class has a ``__construct()`` method, it will be -inherited by all its child classes and will execute just fine - you DON'T -have to explicitly call it unless you want to extend its logic. +The constructor is kept as an empty method for backwards compatibility, so +existing code calling ``parent::__construct()`` will continue to work. +However, such calls are unnecessary and can be safely removed. Step 4: Change database connection handling =========================================== @@ -206,9 +178,6 @@ CodeIgniter versions that have been removed in 3.2.0: - 'sqlite' database driver (no longer shipped with PHP 5.4+; 'sqlite3' is still available) - ``CI_Input::is_cli_request()`` (use :php:func:`is_cli()` instead) -- ``CI_Router::fetch_directory()`` (use ``CI_Router::$directory`` instead) -- ``CI_Router::fetch_class()`` (use ``CI_Router::$class`` instead) -- ``CI_Router::fetch_method()`` (use ``CI_Router::$method`` instead) - ``CI_Config::system_url()`` (encourages insecure practices) - ``CI_Form_validation::prep_for_form()`` (the *prep_for_form* rule) @@ -219,17 +188,10 @@ CodeIgniter versions that have been removed in 3.2.0: - ``nbs()`` :doc:`HTML Helper <../helpers/html_helper>` function (use ``str_repeat()`` with ``' '`` instead) - ``trim_slashes()`` :doc:`String Helper <../helpers/string_helper>` function (use ``trim()`` with ``'/'`` instead) - ``repeater()`` :doc:`String Helper <../helpers/string_helper>` function (use ``str_repeat()`` instead) -- ``read_file()`` :doc:`File Helper <../helpers/file_helper>` function (use ``file_get_contents()`` instead) - ``form_prep()`` :doc:`Form Helper <../helpers/form_helper>` function (use :php:func:`html_escape()` instead) - The entire *Encrypt Library* (the newer :doc:`Encryption Library <../libraries/encryption>` is still available) - The entire *Javascript Library* (it was always experimental in the first place) - -- The entire *Email Helper*, which only had two functions: - - - ``valid_email()`` (use ``filter_var($email, FILTER_VALIDATE_EMAIL)`` instead) - - ``send_email()`` (use ``mail()`` instead) - - The entire *Smiley Helper* (an archived version is available on GitHub: `bcit-ci/ci3-smiley-helper `_) - The ``$_after`` parameter from :doc:`Database Forge <../database/forge>` method ``add_column()``. From a39221a41189eae8b037a7ad45c4fb831d0b1bd3 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sun, 22 Feb 2026 09:22:28 -0700 Subject: [PATCH 49/68] fix: cast ctype_digit() arguments to string to resolve PHP 8.1+ deprecation notice. Fix #41. --- system/database/DB_driver.php | 2 +- system/database/drivers/oci8/oci8_driver.php | 4 ++-- system/database/drivers/postgre/postgre_driver.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/system/database/DB_driver.php b/system/database/DB_driver.php index 6cbd7026e91..9e398c7101a 100644 --- a/system/database/DB_driver.php +++ b/system/database/DB_driver.php @@ -1349,7 +1349,7 @@ public function escape_identifiers($item, $split = TRUE) 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) + elseif (ctype_digit((string) $item) OR $item[0] === "'" OR ($this->_escape_char !== '"' && $item[0] === '"') OR strpos($item, '(') !== FALSE) { return $item; } diff --git a/system/database/drivers/oci8/oci8_driver.php b/system/database/drivers/oci8/oci8_driver.php index 6f8b21d750f..7640ab41f5f 100644 --- a/system/database/drivers/oci8/oci8_driver.php +++ b/system/database/drivers/oci8/oci8_driver.php @@ -177,7 +177,7 @@ public function __construct($params) return; } elseif ($this->hostname !== '' && strpos($this->hostname, '/') === FALSE && strpos($this->hostname, ':') === FALSE - && (( ! empty($this->port) && ctype_digit($this->port)) OR $this->database !== '')) + && (( ! empty($this->port) && ctype_digit((string) $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 @@ -187,7 +187,7 @@ public function __construct($params) * that the database field is a service name. */ $this->dsn = $this->hostname - .(( ! empty($this->port) && ctype_digit($this->port)) ? ':'.$this->port : '') + .(( ! empty($this->port) && ctype_digit((string) $this->port)) ? ':'.$this->port : '') .($this->database !== '' ? '/'.ltrim($this->database, '/') : ''); if (preg_match($valid_dsns['ec'], $this->dsn)) diff --git a/system/database/drivers/postgre/postgre_driver.php b/system/database/drivers/postgre/postgre_driver.php index 1cd473f2b5d..fe7750f6841 100644 --- a/system/database/drivers/postgre/postgre_driver.php +++ b/system/database/drivers/postgre/postgre_driver.php @@ -95,7 +95,7 @@ protected function _build_dsn() $this->hostname === '' OR $this->dsn = 'host='.$this->hostname.' '; - if ( ! empty($this->port) && ctype_digit($this->port)) + if ( ! empty($this->port) && ctype_digit((string) $this->port)) { $this->dsn .= 'port='.$this->port.' '; } From 020416071028ef31c0a54e29600130af9253d1b5 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sun, 22 Feb 2026 09:47:07 -0700 Subject: [PATCH 50/68] fix: cast mt_rand() arguments to int to resolve PHP 8.1+ deprecation notices in captcha_helper. Fix #40. --- system/helpers/captcha_helper.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/system/helpers/captcha_helper.php b/system/helpers/captcha_helper.php index 2bee0082efc..e5a1aa318a2 100644 --- a/system/helpers/captcha_helper.php +++ b/system/helpers/captcha_helper.php @@ -259,8 +259,8 @@ function create_captcha($data) // ----------------------------------- $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); + $x_axis = mt_rand(6, (int) ((360 / $length) - 16)); + $y_axis = ($angle >= 0) ? mt_rand((int) $img_height, (int) $img_width) : mt_rand(6, (int) $img_height); // Create image // PHP.net recommends imagecreatetruecolor(), but it isn't always available @@ -315,13 +315,13 @@ function create_captcha($data) if ($use_font === FALSE) { ($font_size > 5) && $font_size = 5; - $x = mt_rand(0, $img_width / ($length / 3)); + $x = mt_rand(0, (int) ($img_width / ($length / 3))); $y = 0; } else { ($font_size > 30) && $font_size = 30; - $x = mt_rand(0, $img_width / ($length / 1.5)); + $x = mt_rand(0, (int) ($img_width / ($length / 1.5))); $y = $font_size + 2; } @@ -329,13 +329,13 @@ function create_captcha($data) { if ($use_font === FALSE) { - $y = mt_rand(0 , $img_height / 2); + $y = mt_rand(0, (int) ($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); + $y = mt_rand((int) ($img_height / 2), (int) ($img_height - 3)); imagettftext($im, $font_size, $angle, $x, $y, $colors['text'], $font_path, $word[$i]); $x += $font_size; } From a87d76d1bde6fe5741104bf18605c04f3779ced0 Mon Sep 17 00:00:00 2001 From: Brian Carey Date: Sun, 22 Feb 2026 11:50:13 -0500 Subject: [PATCH 51/68] Fix deprecation notice for passing a null to preg_replace (#39) --- system/core/Common.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/core/Common.php b/system/core/Common.php index c7bb34549b5..bcb1f1aa36d 100644 --- a/system/core/Common.php +++ b/system/core/Common.php @@ -727,7 +727,7 @@ function remove_invisible_characters($str, $url_encoded = TRUE) do { - $str = preg_replace($non_displayables, '', $str, -1, $count); + $str = preg_replace($non_displayables, '', (string)$str, -1, $count); } while ($count); From dfa6f32d5f4448ca351385a3653619fdbd3641f7 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sun, 22 Feb 2026 10:42:17 -0700 Subject: [PATCH 52/68] fix: add PHPUnit 8.x to composer.json to restore PHP 7.2 test support. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index fc6e74b2857..81c3ea5065e 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,6 @@ }, "require-dev": { "mikey179/vfsstream": "1.6.*", - "phpunit/phpunit": "4.* || 5.* || 9.*" + "phpunit/phpunit": "4.* || 5.* || 8.* || 9.*" } } From 6ff57178d8b15154c846a3b73acc8d050f4c0dc4 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Sun, 22 Feb 2026 12:28:00 -0700 Subject: [PATCH 53/68] chore: bump version to 3.4.2. --- system/core/CodeIgniter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/core/CodeIgniter.php b/system/core/CodeIgniter.php index 9bb4de8db66..ff3e150cbe3 100644 --- a/system/core/CodeIgniter.php +++ b/system/core/CodeIgniter.php @@ -56,7 +56,7 @@ * @var string * */ - const CI_VERSION = '3.4.1'; + const CI_VERSION = '3.4.2'; /* * ------------------------------------------------------ From 17ea1c68c3831c2da62ff29a4041fcbef626a203 Mon Sep 17 00:00:00 2001 From: czirosan <44802400+czirosan@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:02:41 +0000 Subject: [PATCH 54/68] Fix Redis session driver returning success on failed connection. The open() method in Session_redis_driver returns $this->_success even when Redis::connect() fails (the else branch on a failed connection). This causes PHP's session_start() to believe the handler is ready, but subsequent read() calls fail, producing: session_start(): Failed to read session data: user (path: /var/lib/php/sessions) This was originally reported and fixed in the upstream bcit-ci/CodeIgniter repo: Issue: https://github.com/bcit-ci/CodeIgniter/issues/5779 PR: https://github.com/bcit-ci/CodeIgniter/pull/5781 The fix was merged into CI3 for the 3.1.11 milestone but appears to have been lost when the 3.2.0-dev branch was created, which this fork is based on. The fix changes the failed-connection else branch to log an error and fall through to return $this->_failure instead of incorrectly returning $this->_success. --- system/libraries/Session/drivers/Session_redis_driver.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/system/libraries/Session/drivers/Session_redis_driver.php b/system/libraries/Session/drivers/Session_redis_driver.php index 43dd8415dce..22a27f2839d 100644 --- a/system/libraries/Session/drivers/Session_redis_driver.php +++ b/system/libraries/Session/drivers/Session_redis_driver.php @@ -217,9 +217,7 @@ public function open($save_path, $name) } else { - $this->_redis = $redis; - $this->php5_validate_id(); - return $this->_success; + log_message('error', 'Session: Unable to connect to Redis with the configured settings.'); } return $this->_failure; From adc059a93d06a3fdb3d57eee5b15dd3b42d99e22 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Wed, 18 Mar 2026 17:16:12 -0600 Subject: [PATCH 55/68] fix: restore form_prep() removed in 3.2.0-dev. --- system/helpers/form_helper.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/system/helpers/form_helper.php b/system/helpers/form_helper.php index 191fc7e6d98..5ae12c64e80 100644 --- a/system/helpers/form_helper.php +++ b/system/helpers/form_helper.php @@ -676,6 +676,25 @@ function form_close($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')) { /** From d9a6c4cc655a886a8f44f27bff88a56498aef530 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Thu, 19 Mar 2026 07:08:22 -0600 Subject: [PATCH 56/68] fix: restore do_hash() removed in 3.2.0-dev. --- system/helpers/security_helper.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/system/helpers/security_helper.php b/system/helpers/security_helper.php index 54851a0947c..4e39fb40665 100644 --- a/system/helpers/security_helper.php +++ b/system/helpers/security_helper.php @@ -83,6 +83,29 @@ function sanitize_filename($filename) // ------------------------------------------------------------------------ +if ( ! function_exists('do_hash')) +{ + /** + * Hash encode a string + * + * @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')) { /** From f5cc5c74f5805e59cbe53f5bd5d17655c5b3dfb3 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Thu, 19 Mar 2026 07:22:44 -0600 Subject: [PATCH 57/68] fix: restore br() and nbs() removed in 3.2.0-dev. --- system/helpers/html_helper.php | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/system/helpers/html_helper.php b/system/helpers/html_helper.php index 98998c7c437..a7fad27434e 100644 --- a/system/helpers/html_helper.php +++ b/system/helpers/html_helper.php @@ -389,3 +389,37 @@ function meta($name = '', $content = '', $type = 'name', $newline = "\n") 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('
', $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(' ', $num); + } +} From 527389727a52e31bcac6024fb821d05363aa6588 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Thu, 19 Mar 2026 07:23:27 -0600 Subject: [PATCH 58/68] fix: restore trim_slashes() and repeater() removed in 3.2.0-dev. --- system/helpers/string_helper.php | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/system/helpers/string_helper.php b/system/helpers/string_helper.php index 0cd87e91515..aa3d180d6e2 100644 --- a/system/helpers/string_helper.php +++ b/system/helpers/string_helper.php @@ -50,6 +50,32 @@ // ------------------------------------------------------------------------ +if ( ! function_exists('trim_slashes')) +{ + /** + * Trim Slashes + * + * Removes any leading/trailing slashes from a string: + * + * /this/that/theother/ + * + * becomes: + * + * this/that/theother + * + * @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')) { /** @@ -254,3 +280,22 @@ function alternator() return $args[($i++ % count($args))]; } } + +// ------------------------------------------------------------------------ + +if ( ! function_exists('repeater')) +{ + /** + * Repeater function + * + * @deprecated 3.0.0 Use PHP's native str_repeat() instead + * + * @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) : ''; + } +} From 74f6dfc86e85318e4d008eb98b9315e067e33b02 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Thu, 19 Mar 2026 07:25:04 -0600 Subject: [PATCH 59/68] fix: restore standard_date() and nice_date() removed in 3.2.0-dev. --- system/helpers/date_helper.php | 102 +++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/system/helpers/date_helper.php b/system/helpers/date_helper.php index 12fa872e321..6cf1eee21e6 100644 --- a/system/helpers/date_helper.php +++ b/system/helpers/date_helper.php @@ -123,6 +123,42 @@ function mdate($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 internally by + * date() as well. + * + * @deprecated 3.1.3 Use PHP's native date() with DateTime constants + * + * @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')) { /** @@ -451,6 +487,72 @@ function human_to_unix($datestr = '') // ------------------------------------------------------------------------ +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')) { /** From ebce7065afb6303fc7bf23dfc19e23e22c6d5018 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Thu, 19 Mar 2026 07:27:12 -0600 Subject: [PATCH 60/68] fix: restore is_cli_request() removed in 3.2.0-dev. --- system/core/Input.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/system/core/Input.php b/system/core/Input.php index 62a1d89f87a..47e71f9ae95 100644 --- a/system/core/Input.php +++ b/system/core/Input.php @@ -662,6 +662,22 @@ public function is_ajax_request() // -------------------------------------------------------------------- + /** + * Is CLI request? + * + * Test to see if a request was made from the command line. + * + * @deprecated 3.0.0 Use is_cli() instead + * @see is_cli() + * @return bool + */ + public function is_cli_request() + { + return is_cli(); + } + + // -------------------------------------------------------------------- + /** * Get Request Method * From 9ae1d9af479331c06e306eb2cccf84c0f6a2e887 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Thu, 19 Mar 2026 07:29:40 -0600 Subject: [PATCH 61/68] fix: restore prep_for_form() removed in 3.2.0-dev. --- system/libraries/Form_validation.php | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/system/libraries/Form_validation.php b/system/libraries/Form_validation.php index dd1685db193..71587f7cb29 100644 --- a/system/libraries/Form_validation.php +++ b/system/libraries/Form_validation.php @@ -105,6 +105,14 @@ class CI_Form_validation { */ protected $error_string = ''; + /** + * Whether the form data has been through validation and had errors + * + * @deprecated 3.0.6 + * @var bool + */ + protected $_safe_form_data = FALSE; + /** * Custom data to validate * @@ -479,6 +487,7 @@ public function run($config = NULL, &$data = NULL) if ( ! empty($this->_error_array)) { + $this->_safe_form_data = TRUE; return FALSE; } @@ -1524,6 +1533,38 @@ public function valid_base64($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(''', '"', '<', '>'), stripslashes($data)); + } + + // -------------------------------------------------------------------- + /** * Prep URL * From 4a80a3bab444de1422ff0468bea3ac784ecd3b8d Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Thu, 19 Mar 2026 07:31:24 -0600 Subject: [PATCH 62/68] fix: restore system_url() removed in 3.2.0-dev. --- system/core/Config.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/system/core/Config.php b/system/core/Config.php index 4efe1e1ec1b..e5476509efb 100644 --- a/system/core/Config.php +++ b/system/core/Config.php @@ -325,6 +325,20 @@ public function base_url($uri = '', $protocol = NULL) // ------------------------------------------------------------- + /** + * System URL + * + * @deprecated 3.0.0 + * @return string + */ + public function system_url() + { + $x = explode('/', preg_replace('|/*(.+?)/*$|', '\\1', BASEPATH)); + return $this->slash_item('base_url').end($x).'/'; + } + + // ------------------------------------------------------------- + /** * Build URI string * From e62bacdfd203f4d519cac5bc141bfa1a9219eb28 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Thu, 19 Mar 2026 08:39:48 -0600 Subject: [PATCH 63/68] docs: rework upgrade guide and README for the 3.2+ maintenance fork. --- readme.rst | 18 ++---- .../source/installation/upgrade_320.rst | 57 +++++++++---------- .../source/installation/upgrading.rst | 2 +- 3 files changed, 32 insertions(+), 45 deletions(-) diff --git a/readme.rst b/readme.rst index e5b27d1d203..5407e33a557 100644 --- a/readme.rst +++ b/readme.rst @@ -93,17 +93,9 @@ If you prefer the traditional approach of replacing the system directory: **Upgrading from Original CI3** -⚠️ **Important:** This fork is based on the unreleased CodeIgniter 3.2.0-dev version, not the stable 3.1.13. If you're upgrading from CI 3.1.x, please read the upgrade guide for any changes that may affect your application. +This fork is based on the unreleased CodeIgniter 3.2.0-dev. For most +applications the upgrade is straightforward: install via Composer, +update your `$system_path`, and review the upgrade guide. -**Please review the upgrade guide:** `upgrade_320.rst `_ - -Note: The upgrade guide has been updated to reflect functionality we've restored for backward compatibility (Cart library, Email helper, etc.). - -Steps to upgrade: - -1. Review the upgrade guide for breaking changes between 3.1.x and 3.2.0 -2. Install via Composer as shown above -3. Update the `$system_path` in your `index.php` -4. Apply any necessary changes from the upgrade guide to your application -5. Your existing `application/` directory remains mostly unchanged (except for items noted in the upgrade guide) -6. Test thoroughly with your PHP version (especially if using PHP 8.2+) +The upgrade guide covers both 3.1.x and 3.2-dev users: +`upgrade_320.rst `_ diff --git a/user_guide_src/source/installation/upgrade_320.rst b/user_guide_src/source/installation/upgrade_320.rst index d622d60d243..c6e20b11867 100644 --- a/user_guide_src/source/installation/upgrade_320.rst +++ b/user_guide_src/source/installation/upgrade_320.rst @@ -1,6 +1,11 @@ -############################# -Upgrading from 3.1.x to 3.2.x -############################# +####################################### +Upgrading from 3.1.x or 3.2-dev to 3.2+ +####################################### + +This guide covers upgrading to this maintenance fork, which is based on +the unreleased CodeIgniter 3.2.0-dev. Many unnecessary breaking changes +from 3.2.0-dev have been reverted to preserve backward compatibility. +This is the same guide whether you are coming from 3.1.x or 3.2-dev. Before performing an update you should take your site offline by replacing the index.php file with a static one. @@ -8,7 +13,16 @@ replacing the index.php file with a static one. Step 1: Update your CodeIgniter files ===================================== -Replace all files and directories in your *system/* directory. +Install via Composer (recommended):: + + composer require pocketarc/codeigniter + +Then update the ``$system_path`` in your ``index.php``:: + + $system_path = 'vendor/pocketarc/codeigniter/system'; + +Alternatively, you can manually replace all files and directories in +your *system/* directory. .. note:: If you have any custom developed files in these directories, please make copies of them first. @@ -16,13 +30,9 @@ Replace all files and directories in your *system/* directory. Step 2: Check your PHP version ============================== -We recommend always running versions that are `currently supported -`_, which right now is at least PHP 5.6. - -PHP 5.3.x versions are now officially not supported by CodeIgniter, and while 5.4.8+ -may be at least runnable, we strongly discourage you from using any PHP versions below -the ones listed on the `PHP.net Supported Versions `_ -page. +This fork supports PHP 5.4 through 8.5+. We recommend running a +`currently supported `_ +PHP version (8.4 or newer). Step 3: Calls to ``CI_Model::__construct()`` (optional cleanup) =============================================================== @@ -169,7 +179,7 @@ Step 10: Remove usage of previously deprecated functionalities ============================================================== The following is a list of functionalities deprecated in previous -CodeIgniter versions that have been removed in 3.2.0: +CodeIgniter versions that have been removed in 3.2+: - ``$config['allow_get_array']`` (use ``$_GET = array();`` instead) - ``$config['standardize_newlines']`` @@ -177,19 +187,6 @@ CodeIgniter versions that have been removed in 3.2.0: - 'sqlite' database driver (no longer shipped with PHP 5.4+; 'sqlite3' is still available) -- ``CI_Input::is_cli_request()`` (use :php:func:`is_cli()` instead) -- ``CI_Config::system_url()`` (encourages insecure practices) -- ``CI_Form_validation::prep_for_form()`` (the *prep_for_form* rule) - -- ``standard_date()`` :doc:`Date Helper <../helpers/date_helper>` function (use ``date()`` instead) -- ``nice_date()`` :doc:`Date Helper <../helpers/date_helper>` function (use ``DateTime::format()`` instead) -- ``do_hash()`` :doc:`Security Helper <../helpers/security_helper>` function (use ``hash()`` instead) -- ``br()`` :doc:`HTML Helper <../helpers/html_helper>` function (use ``str_repeat()`` with ``'
'`` instead) -- ``nbs()`` :doc:`HTML Helper <../helpers/html_helper>` function (use ``str_repeat()`` with ``' '`` instead) -- ``trim_slashes()`` :doc:`String Helper <../helpers/string_helper>` function (use ``trim()`` with ``'/'`` instead) -- ``repeater()`` :doc:`String Helper <../helpers/string_helper>` function (use ``str_repeat()`` instead) -- ``form_prep()`` :doc:`Form Helper <../helpers/form_helper>` function (use :php:func:`html_escape()` instead) - - The entire *Encrypt Library* (the newer :doc:`Encryption Library <../libraries/encryption>` is still available) - The entire *Javascript Library* (it was always experimental in the first place) - The entire *Smiley Helper* (an archived version is available on GitHub: `bcit-ci/ci3-smiley-helper `_) @@ -238,14 +235,12 @@ The ``$curs_id`` property is also removed. If you were using those, you can create your own cursors via ``oci_new_cursor()`` and the publicly accessible ``$conn_id``. -Stop 14: Replace $config['log_file_extension'] with $config['log_filename'] in application/config/config.php -============================================================================================================ +Step 14: Check log filename configuration in application/config/config.php +========================================================================== You can now specify the full log filename via ``$config['log_filename']``. Add this configuration option to your **application/config/config.php**, if you haven't copied the new one over. -The previously existing ``$config['log_file_extension']`` option has been -removed and no longer works. However, its functionality is essentially -integrated into the new ``$config['log_filename']``, since it includes the -filename extension in itself. +The ``$config['log_file_extension']`` option still works as a fallback, +but ``$config['log_filename']`` takes precedence when set. diff --git a/user_guide_src/source/installation/upgrading.rst b/user_guide_src/source/installation/upgrading.rst index 6924dde485d..c87fea5e065 100644 --- a/user_guide_src/source/installation/upgrading.rst +++ b/user_guide_src/source/installation/upgrading.rst @@ -8,7 +8,7 @@ upgrading from. .. toctree:: :titlesonly: - Upgrading from 3.1.12+ to 3.2.x + Upgrading from 3.1.x or 3.2-dev to 3.2+ Upgrading from 3.1.13 to 3.1.14 Upgrading from 3.1.12 to 3.1.13 Upgrading from 3.1.11 to 3.1.12 From 56504ca8c5719cedb145c4c250810a8a923ab48b Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Thu, 19 Mar 2026 08:50:47 -0600 Subject: [PATCH 64/68] fix: restore 'unique' and 'encrypt' aliases in random_string() removed in 3.2.0-dev. --- system/helpers/string_helper.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system/helpers/string_helper.php b/system/helpers/string_helper.php index aa3d180d6e2..f2fc205c5b8 100644 --- a/system/helpers/string_helper.php +++ b/system/helpers/string_helper.php @@ -227,8 +227,10 @@ function random_string($type = 'alnum', $len = 8) break; } return substr(str_shuffle(str_repeat($pool, ceil($len / strlen($pool)))), 0, $len); + case 'unique': case 'md5': return md5(uniqid(mt_rand())); + case 'encrypt': case 'sha1': return sha1(uniqid(mt_rand(), TRUE)); } From c3fb46dc7c0ff57bb5baa49264028dd0af0fec0a Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Thu, 19 Mar 2026 08:53:14 -0600 Subject: [PATCH 65/68] fix: restore 'dash' and 'underscore' aliases in url_title() removed in 3.2.0-dev. --- system/helpers/url_helper.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/system/helpers/url_helper.php b/system/helpers/url_helper.php index e3c9bc0a4fa..1a932a7bb66 100644 --- a/system/helpers/url_helper.php +++ b/system/helpers/url_helper.php @@ -478,6 +478,15 @@ function prep_url($str = '') */ function url_title($str, $separator = '-', $lowercase = FALSE) { + if ($separator === 'dash') + { + $separator = '-'; + } + elseif ($separator === 'underscore') + { + $separator = '_'; + } + $q_separator = preg_quote($separator, '#'); $trans = array( From cb11ea17a400ce822e9df18eb6edc3cc8815f4f3 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Thu, 19 Mar 2026 08:54:25 -0600 Subject: [PATCH 66/68] fix: restore 'anchor_class' option in Pagination removed in 3.2.0-dev. --- system/libraries/Pagination.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/system/libraries/Pagination.php b/system/libraries/Pagination.php index 7f1ed7739c8..38ba8d9e735 100644 --- a/system/libraries/Pagination.php +++ b/system/libraries/Pagination.php @@ -358,6 +358,12 @@ public function __construct($params = array()) */ public function initialize(array $params = array()) { + if (isset($params['anchor_class'])) + { + empty($params['anchor_class']) OR $params['attributes']['class'] = $params['anchor_class']; + unset($params['anchor_class']); + } + if (isset($params['attributes']) && is_array($params['attributes'])) { $this->_parse_attributes($params['attributes']); From 34340cc6c75f14c92c7678387cb3b9e11f558e53 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Thu, 19 Mar 2026 08:55:15 -0600 Subject: [PATCH 67/68] fix: restore $_after parameter in add_column() removed in 3.2.0-dev. --- system/database/DB_forge.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/system/database/DB_forge.php b/system/database/DB_forge.php index 36679a4649d..26a591947a6 100644 --- a/system/database/DB_forge.php +++ b/system/database/DB_forge.php @@ -561,13 +561,18 @@ public function rename_table($table_name, $new_table_name) * @param array $field Column definition * @return bool */ - public function add_column($table, $field) + 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) { + if ($_after !== NULL && is_array($field[$k]) && ! isset($field[$k]['after'])) + { + $field[$k]['after'] = $_after; + } + $this->add_field(array($k => $field[$k])); } From 53115e6cdfd3338374fcbacfad6d6d9e70d2ebb3 Mon Sep 17 00:00:00 2001 From: Bruno Moreira Date: Thu, 19 Mar 2026 08:55:49 -0600 Subject: [PATCH 68/68] docs: remove restored items from 3.2+ upgrade removal list. --- user_guide_src/source/installation/upgrade_320.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/user_guide_src/source/installation/upgrade_320.rst b/user_guide_src/source/installation/upgrade_320.rst index c6e20b11867..8ed23afa27a 100644 --- a/user_guide_src/source/installation/upgrade_320.rst +++ b/user_guide_src/source/installation/upgrade_320.rst @@ -191,10 +191,6 @@ CodeIgniter versions that have been removed in 3.2+: - The entire *Javascript Library* (it was always experimental in the first place) - The entire *Smiley Helper* (an archived version is available on GitHub: `bcit-ci/ci3-smiley-helper `_) -- The ``$_after`` parameter from :doc:`Database Forge <../database/forge>` method ``add_column()``. -- The ``anchor_class`` option from :doc:`Pagination Library <../libraries/pagination>` (use ``class`` instead). -- The ``unique`` and ``encrypt`` options from :doc:`String Helper <../helpers/string_helper>` function ``random_string()``. -- The ``underscore`` and ``dash`` options from :doc:`URL Helper <../helpers/url_helper>`` function :php:func:`url_title()`. - The ``$img_path``, ``$img_url`` and ``$font_path`` parameters from :doc:`CAPCHA Helper <../helpers/captcha_helper>` function :php:func:`create_captcha()` (pass as array options instead).