diff --git a/system/core/Input.php b/system/core/Input.php index a2fca18f613..90552e8ee6a 100644 --- a/system/core/Input.php +++ b/system/core/Input.php @@ -390,7 +390,7 @@ public function set_cookie($name, $value = '', $expire = 0, $domain = '', $path 'httponly' => $httponly, 'samesite' => $samesite, ); - setcookie($prefix.$name, (string) $value, $setcookie_options); + setcookie($prefix.$name, $value === NULL ? '' : $value, $setcookie_options); } // -------------------------------------------------------------------- diff --git a/system/core/Output.php b/system/core/Output.php index 7a908bf25c3..7c4a759c2b3 100644 --- a/system/core/Output.php +++ b/system/core/Output.php @@ -558,8 +558,7 @@ public function _display($output = NULL) public function _write_cache($output) { $CI =& get_instance(); - $path = $CI->config->item('cache_path'); - $cache_path = ($path === '') ? APPPATH.'cache'.DIRECTORY_SEPARATOR : rtrim($path, '/\\').DIRECTORY_SEPARATOR; + $cache_path = $this->_get_cache_path($CI->config->item('cache_path')); if ( ! is_dir($cache_path) OR ! is_really_writable($cache_path)) { @@ -660,8 +659,7 @@ public function _write_cache($output) */ public function _display_cache(&$CFG, &$URI) { - $path = $CFG->item('cache_path'); - $cache_path = ($path === '') ? APPPATH.'cache'.DIRECTORY_SEPARATOR : rtrim($path, '/\\').DIRECTORY_SEPARATOR; + $cache_path = $this->_get_cache_path($CFG->item('cache_path')); // Build the file path. The file name is an MD5 hash of the full URI $uri = $CFG->item('base_url').$CFG->slash_item('index_page').$URI->uri_string; @@ -738,8 +736,7 @@ public function _display_cache(&$CFG, &$URI) public function delete_cache($uri = '') { $CI =& get_instance(); - $path = $CI->config->item('cache_path'); - $cache_path = ($path === '') ? APPPATH.'cache'.DIRECTORY_SEPARATOR : rtrim($path, '/\\').DIRECTORY_SEPARATOR; + $cache_path = $this->_get_cache_path($CI->config->item('cache_path')); if ( ! is_dir($cache_path)) { @@ -777,6 +774,19 @@ public function delete_cache($uri = '') // -------------------------------------------------------------------- + /** + * Get cache path + * + * @param string $path The configured cache_path value + * @return string Cache directory path with a trailing directory separator + */ + protected function _get_cache_path($path) + { + return empty($path) ? APPPATH.'cache'.DIRECTORY_SEPARATOR : rtrim($path, '/\\').DIRECTORY_SEPARATOR; + } + + // -------------------------------------------------------------------- + /** * Set Cache Header * diff --git a/system/helpers/captcha_helper.php b/system/helpers/captcha_helper.php index 8d24ac93bdc..bade01724ea 100644 --- a/system/helpers/captcha_helper.php +++ b/system/helpers/captcha_helper.php @@ -133,8 +133,7 @@ function create_captcha($data) @closedir($current_dir); // This variable will later be used later to determine whether we write to disk or output a data:image URI - // %F is locale-independent, unlike float-to-string conversion on PHP < 8.0 - $img_filename = sprintf('%.6F', $now).'.png'; + $img_filename = $now.'.png'; } else { @@ -346,20 +345,9 @@ function create_captcha($data) } else { - // I don't see an easier way to get the image contents without writing to file - $buffer = fopen('php://memory', 'wb+'); - imagepng($im, $buffer); - rewind($buffer); - $img_src = ''; - - // fread() will return an empty string (not FALSE) after the entire contents are read - while (strlen($read = fread($buffer, 4096))) - { - $img_src .= $read; - } - - fclose($buffer); - $img_src = 'data:image/png;base64,'.base64_encode($img_src); + ob_start(); + imagepng($im); + $img_src = 'data:image/png;base64,'.base64_encode(ob_get_clean()); } $img_class = trim($img_class); diff --git a/system/libraries/Cache/drivers/Cache_file.php b/system/libraries/Cache/drivers/Cache_file.php index 3a4be98a9ba..8dabac9dec8 100644 --- a/system/libraries/Cache/drivers/Cache_file.php +++ b/system/libraries/Cache/drivers/Cache_file.php @@ -66,7 +66,7 @@ public function __construct() $CI =& get_instance(); $CI->load->helper('file'); $path = $CI->config->item('cache_path'); - $this->_cache_path = ($path === '') ? APPPATH.'cache/' : $path; + $this->_cache_path = empty($path) ? APPPATH.'cache'.DIRECTORY_SEPARATOR : rtrim($path, '/\\').DIRECTORY_SEPARATOR; } // ------------------------------------------------------------------------ diff --git a/system/libraries/Cache/drivers/Cache_redis.php b/system/libraries/Cache/drivers/Cache_redis.php index ac92d5ad8e1..2dd68e6deba 100644 --- a/system/libraries/Cache/drivers/Cache_redis.php +++ b/system/libraries/Cache/drivers/Cache_redis.php @@ -163,11 +163,17 @@ public function get($key) { $data = $this->_redis->hMGet($key, array('__ci_type', '__ci_value')); - if ($data === FALSE || $this->_redis->sIsMember('_ci_redis_serialized', $key)) + if ($this->_redis->sIsMember('_ci_redis_serialized', $key)) { return FALSE; } + // Also serves scalars that 3.1.x cached as plain strings + if ($data === FALSE) + { + return $this->_redis->get($key); + } + switch ($data['__ci_type']) { case 'array': @@ -261,15 +267,7 @@ public function delete($key) */ public function increment($id, $offset = 1) { - $value = $this->_redis->hIncrBy($id, '__ci_value', $offset); - - if ($value !== FALSE) - { - $this->_redis->hSet($id, '__ci_type', 'integer'); - return $value; - } - - return FALSE; + return $this->_increment($id, $offset); } // ------------------------------------------------------------------------ @@ -283,15 +281,29 @@ public function increment($id, $offset = 1) */ public function decrement($id, $offset = 1) { - $value = $this->_redis->hIncrBy($id, '__ci_value', -$offset); + return $this->_increment($id, -$offset); + } - if ($value !== FALSE) + // ------------------------------------------------------------------------ + + /** + * Add to a counter + * + * @param string $id Cache ID + * @param int $offset Amount to add (negative to subtract) + * @return mixed New value on success or FALSE on failure + */ + protected function _increment($id, $offset) + { + $value = $this->_redis->incrBy($id, $offset); + + if ($value === FALSE) { - $this->_redis->hSet($id, '__ci_type', 'integer'); - return $value; + $value = $this->_redis->hIncrBy($id, '__ci_value', $offset); + $value === FALSE OR $this->_redis->hSet($id, '__ci_type', 'integer'); } - return FALSE; + return $value; } // ------------------------------------------------------------------------ diff --git a/system/libraries/Form_validation.php b/system/libraries/Form_validation.php index 48a9d035b16..9896740e6f1 100644 --- a/system/libraries/Form_validation.php +++ b/system/libraries/Form_validation.php @@ -587,8 +587,6 @@ protected function _reduce_array($array, $keys, $i = 0) /** * Re-populate the _POST array with our finalized and processed data * - * Arrays require bracket notation or an explicit is_array rule. - * * @return void */ protected function _reset_data_array(&$data) diff --git a/system/libraries/Image_lib.php b/system/libraries/Image_lib.php index 948d084f9c4..1f3c4815aeb 100644 --- a/system/libraries/Image_lib.php +++ b/system/libraries/Image_lib.php @@ -1251,8 +1251,7 @@ public function overlay_watermark() imagecopymerge($src_img, $wm_img, $x_axis, $y_axis, 0, 0, $wm_width, $wm_height, $this->wm_opacity); } - // We can preserve transparency for PNG images - if ($this->image_type === 3) + if ($this->image_type === 3 OR $this->image_type === 18) { imagealphablending($src_img, FALSE); imagesavealpha($src_img, TRUE); @@ -1425,8 +1424,7 @@ public function text_watermark() imagestring($src_img, $this->wm_font_size, $x_axis, $y_axis, $this->wm_text, $txt_color); } - // We can preserve transparency for PNG images - if ($this->image_type === 3) + if ($this->image_type === 3 OR $this->image_type === 18) { imagealphablending($src_img, FALSE); imagesavealpha($src_img, TRUE); diff --git a/system/libraries/Session/drivers/Session_redis_driver.php b/system/libraries/Session/drivers/Session_redis_driver.php index bc25293509a..e20413a05f4 100644 --- a/system/libraries/Session/drivers/Session_redis_driver.php +++ b/system/libraries/Session/drivers/Session_redis_driver.php @@ -134,15 +134,17 @@ public function __construct(&$params) { log_message('error', 'Session: No Redis save path configured.'); } - elseif (preg_match('#^unix://([^\?]+)(?\?.+)?$#', $this->_config['save_path'], $matches)) + elseif (preg_match('#^unix://([^\?]+)(?\?.+)?$#i', $this->_config['save_path'], $matches)) { $save_path = array('path' => $matches[1]); } - elseif (preg_match('#^(?:(tcp|tls|ssl)://)?([^:?]+)(?:\:(\d+))?(?\?.+)?#', $this->_config['save_path'], $matches)) + elseif (preg_match('#^(?:(tcp|tls|ssl)://)?([^:?]+)(?:\:(\d+))?(?\?.+)?#i', $this->_config['save_path'], $matches)) { + $scheme = strtolower($matches[1]); + // phpredis needs the tls:// or ssl:// scheme in the host to enable encryption; tcp:// is the default $save_path = array( - 'host' => (in_array($matches[1], array('tls', 'ssl'), TRUE) ? $matches[1].'://' : '').$matches[2], + 'host' => (in_array($scheme, array('tls', 'ssl'), TRUE) ? $scheme.'://' : '').$matches[2], 'port' => empty($matches[3]) ? NULL : $matches[3], 'timeout' => 0.0 // We always pass this to Redis::connect(), so it needs to exist ); diff --git a/tests/codeigniter/core/Output_test.php b/tests/codeigniter/core/Output_test.php index 8c99c1fc5fe..7de57afa57c 100644 --- a/tests/codeigniter/core/Output_test.php +++ b/tests/codeigniter/core/Output_test.php @@ -60,4 +60,35 @@ public function test_get_header() ); } + // -------------------------------------------------------------------- + + public function test_delete_cache_without_trailing_slash() + { + $this->ci_set_config('cache_path', APPPATH.'pagecache'); + $this->ci_set_config('base_url', 'http://example.com/'); + $this->ci_set_config('index_page', 'index.php'); + + $cache_file = md5('http://example.com/index.php/foo'); + $dir = $this->ci_vfs_mkdir('pagecache', $this->ci_app_root); + vfsStream::newFile($cache_file)->at($dir)->setContent('cached'); + + $this->assertTrue($this->output->delete_cache('foo')); + $this->assertFalse($dir->hasChild($cache_file)); + } + + // -------------------------------------------------------------------- + + public function test_delete_cache_without_cache_path() + { + $this->ci_set_config('base_url', 'http://example.com/'); + $this->ci_set_config('index_page', 'index.php'); + + $cache_file = md5('http://example.com/index.php/foo'); + $dir = $this->ci_vfs_mkdir('cache', $this->ci_app_root); + vfsStream::newFile($cache_file)->at($dir)->setContent('cached'); + + $this->assertTrue($this->output->delete_cache('foo')); + $this->assertFalse($dir->hasChild($cache_file)); + } + } diff --git a/tests/codeigniter/database/Postgre_forge_test.php b/tests/codeigniter/database/Postgre_forge_test.php new file mode 100644 index 00000000000..03e8ffcd847 --- /dev/null +++ b/tests/codeigniter/database/Postgre_forge_test.php @@ -0,0 +1,56 @@ +assertTrue($forge->modify_column('users', array( + 'old_name' => array('name' => 'new_name', 'comment' => 'Renamed column') + ))); + + $this->assertSame(array( + 'ALTER TABLE "users" RENAME COLUMN "old_name" TO "new_name"', + 'COMMENT ON COLUMN "users"."new_name" IS \'Renamed column\'' + ), $db->queries); + } + + public function forge_provider() + { + return array(array('CI_DB_postgre_forge'), array('CI_DB_pdo_pgsql_forge')); + } + +} + +class Postgre_forge_db_stub { + + public $dbprefix = ''; + public $db_debug = FALSE; + public $queries = array(); + + public function version() + { + return '15.0'; + } + + public function escape_identifiers($item) + { + return '"'.$item.'"'; + } + + public function escape($str) + { + return "'".str_replace("'", "''", $str)."'"; + } + + public function query($sql) + { + $this->queries[] = $sql; + return TRUE; + } +} diff --git a/tests/codeigniter/helpers/captcha_helper_test.php b/tests/codeigniter/helpers/captcha_helper_test.php index f2d2599f539..7b4d4abf82c 100644 --- a/tests/codeigniter/helpers/captcha_helper_test.php +++ b/tests/codeigniter/helpers/captcha_helper_test.php @@ -38,14 +38,13 @@ public function tear_down() // -------------------------------------------------------------------- - public function test_create_captcha_writes_image_with_fractional_timestamp_filename() + public function test_create_captcha_names_image_after_time() { $captcha = $this->_create_captcha(); $this->assertTrue(is_array($captcha)); - $this->assertEquals(1, preg_match('#^\d{10}\.\d{6}\.png$#', $captcha['filename'])); + $this->assertSame($captcha['time'].'.png', $captcha['filename']); $this->assertTrue(is_file($this->_img_path.$captcha['filename'])); - $this->assertSame(sprintf('%.6F', $captcha['time']).'.png', $captcha['filename']); } // -------------------------------------------------------------------- @@ -148,21 +147,16 @@ public function test_create_captcha_respects_custom_expiration() // -------------------------------------------------------------------- - public function test_create_captcha_filename_is_locale_independent() + public function test_create_captcha_returns_data_uri_without_paths() { - $original = setlocale(LC_NUMERIC, 0); - $locale = setlocale(LC_NUMERIC, 'de_DE.UTF-8', 'de_DE.utf8', 'de_DE', 'de-DE', 'German_Germany.1252', 'German'); + $captcha = create_captcha(array('word' => 'test')); - if ($locale === FALSE) - { - $this->markTestSkipped('No locale with a comma decimal separator is available.'); - } - - $captcha = $this->_create_captcha(); - setlocale(LC_NUMERIC, $original); + $this->assertTrue(is_array($captcha)); + $this->assertNull($captcha['filename']); + $this->assertEquals(1, preg_match('#src="data:image/png;base64,([A-Za-z0-9+/=]+)"#', $captcha['image'], $match)); + $this->assertSame("\x89PNG", substr(base64_decode($match[1]), 0, 4)); - $this->assertEquals(1, preg_match('#^\d{10}\.\d{6}\.png$#', $captcha['filename'])); - $this->assertTrue(is_file($this->_img_path.$captcha['filename'])); + $this->assertFalse(create_captcha(array('word' => 'test', 'img_url' => 'http://example.com/captcha/'))); } // -------------------------------------------------------------------- diff --git a/tests/codeigniter/libraries/Cache_file_test.php b/tests/codeigniter/libraries/Cache_file_test.php new file mode 100644 index 00000000000..fa635316b15 --- /dev/null +++ b/tests/codeigniter/libraries/Cache_file_test.php @@ -0,0 +1,39 @@ +helper('file'); + + $loader = $this->getMockBuilder('CI_Loader')->setMethods(array('helper'))->getMock(); + $this->ci_instance_var('load', $loader); + } + + // -------------------------------------------------------------------- + + public function test_cache_path_without_trailing_slash() + { + $this->ci_set_config('cache_path', APPPATH.'filecache'); + $dir = $this->ci_vfs_mkdir('filecache', $this->ci_app_root); + $cache = new CI_Cache_file(); + + $this->assertTrue($cache->save('foo', 'bar')); + $this->assertTrue($dir->hasChild('foo')); + $this->assertEquals('bar', $cache->get('foo')); + } + + // -------------------------------------------------------------------- + + public function test_cache_path_without_config_key() + { + $this->ci_set_config(); + $dir = $this->ci_vfs_mkdir('cache', $this->ci_app_root); + $cache = new CI_Cache_file(); + + $this->assertTrue($cache->save('foo', 'bar')); + $this->assertTrue($dir->hasChild('foo')); + } + +} diff --git a/tests/codeigniter/libraries/Cache_redis_test.php b/tests/codeigniter/libraries/Cache_redis_test.php new file mode 100644 index 00000000000..814d0e63c7c --- /dev/null +++ b/tests/codeigniter/libraries/Cache_redis_test.php @@ -0,0 +1,144 @@ +redis = new Cache_redis_server_stub(); + + $class = new ReflectionClass('CI_Cache_redis'); + $this->cache = $class->newInstanceWithoutConstructor(); + + $property = new ReflectionProperty('CI_Cache_redis', '_redis'); + if (PHP_VERSION_ID < 80100) + { + $property->setAccessible(TRUE); + } + $property->setValue($this->cache, $this->redis); + } + + // -------------------------------------------------------------------- + + public function test_increment_creates_plain_counter() + { + $this->assertSame(1, $this->cache->increment('hits')); + $this->assertSame(3, $this->cache->increment('hits', 2)); + $this->assertSame(2, $this->cache->decrement('hits')); + + $this->assertSame('2', $this->redis->strings['hits']); + $this->assertEquals(2, $this->cache->get('hits')); + } + + // -------------------------------------------------------------------- + + public function test_increment_existing_plain_counter() + { + $this->redis->strings['hits'] = '5'; + + $this->assertSame(6, $this->cache->increment('hits')); + $this->assertSame(4, $this->cache->decrement('hits', 2)); + } + + // -------------------------------------------------------------------- + + public function test_increment_saved_value() + { + $this->redis->hashes['hits'] = array('__ci_type' => 'integer', '__ci_value' => '5'); + + $this->assertSame(6, $this->cache->increment('hits')); + $this->assertSame(5, $this->cache->decrement('hits')); + $this->assertSame(5, $this->cache->get('hits')); + } + + // -------------------------------------------------------------------- + + public function test_increment_non_numeric_value() + { + $this->redis->strings['name'] = 'bar'; + $this->redis->hashes['data'] = array('__ci_type' => 'string', '__ci_value' => 'bar'); + + $this->assertFalse($this->cache->increment('name')); + $this->assertFalse($this->cache->increment('data')); + $this->assertSame('string', $this->redis->hashes['data']['__ci_type']); + } + +} + +class Cache_redis_server_stub { + + public $strings = array(); + public $hashes = array(); + + public function incrBy($key, $offset) + { + if (isset($this->hashes[$key]) OR (isset($this->strings[$key]) && ! ctype_digit(ltrim($this->strings[$key], '-')))) + { + return FALSE; + } + + $value = (isset($this->strings[$key]) ? (int) $this->strings[$key] : 0) + $offset; + $this->strings[$key] = (string) $value; + return $value; + } + + public function hIncrBy($key, $field, $offset) + { + if (isset($this->strings[$key]) OR (isset($this->hashes[$key][$field]) && ! ctype_digit(ltrim($this->hashes[$key][$field], '-')))) + { + return FALSE; + } + + $value = (isset($this->hashes[$key][$field]) ? (int) $this->hashes[$key][$field] : 0) + $offset; + $this->hashes[$key][$field] = (string) $value; + return $value; + } + + public function hSet($key, $field, $value) + { + if (isset($this->strings[$key])) + { + return FALSE; + } + + $this->hashes[$key][$field] = (string) $value; + return 1; + } + + public function hMGet($key, $fields) + { + if (isset($this->strings[$key])) + { + return FALSE; + } + + $values = array(); + foreach ($fields as $field) + { + $values[$field] = isset($this->hashes[$key][$field]) ? $this->hashes[$key][$field] : FALSE; + } + + return $values; + } + + public function get($key) + { + if (isset($this->hashes[$key])) + { + return FALSE; + } + + return isset($this->strings[$key]) ? $this->strings[$key] : FALSE; + } + + public function sIsMember($key, $member) + { + return FALSE; + } + + public function close() + { + return TRUE; + } +} diff --git a/tests/codeigniter/libraries/Form_validation_test.php b/tests/codeigniter/libraries/Form_validation_test.php index 1bafd50c350..6e1029cc6b7 100644 --- a/tests/codeigniter/libraries/Form_validation_test.php +++ b/tests/codeigniter/libraries/Form_validation_test.php @@ -642,6 +642,104 @@ public function test_validated_data_assignment() $_POST = array(); } + public function test_set_rules_null_label_uses_field_name() + { + $_POST = array('email' => ''); + $this->form_validation->set_message('required', 'The {field} field is required.'); + $this->form_validation->set_rules('email', NULL, 'required'); + + $this->assertFalse($this->form_validation->run()); + $this->assertEquals('

The email field is required.

', $this->form_validation->error('email')); + + $_POST = array(); + } + + public function test_is_array_rule_on_bracket_field() + { + $_POST = array('tags' => array('a', 'b')); + $this->form_validation->set_rules('tags[]', 'Tags', 'required|is_array'); + $this->assertTrue($this->form_validation->run()); + + $this->form_validation->reset_validation(); + $this->form_validation->set_data(array('tags' => array())); + $this->form_validation->set_rules('tags[]', 'Tags', 'required|is_array'); + $this->assertFalse($this->form_validation->run()); + + $_POST = array(); + } + + public function test_is_array_rule_on_bracket_field_passes_whole_array_to_callable() + { + $seen = NULL; + $_POST = array('tags' => array('a', 'b')); + $this->form_validation->set_rules('tags[]', 'Tags', array( + 'is_array', + array('capture', function ($value) use (&$seen) { + $seen = $value; + return TRUE; + }) + )); + + $this->assertTrue($this->form_validation->run()); + $this->assertEquals(array('a', 'b'), $seen); + + $_POST = array(); + } + + public function test_is_array_rule_on_keyed_field() + { + $_POST = array('user' => array('roles' => array('admin', 'editor'))); + $this->form_validation->set_rules('user[roles]', 'Roles', 'required|is_array'); + + $this->assertTrue($this->form_validation->run()); + $this->assertEquals(array('admin', 'editor'), $_POST['user']['roles']); + + $_POST = array(); + } + + public function test_is_array_rule_keeps_array_after_run() + { + $_POST = array('tags' => array('a', 'b')); + $this->form_validation->set_rules('tags', 'Tags', 'required|is_array'); + + $this->assertTrue($this->form_validation->run()); + $this->assertEquals(array('a', 'b'), $_POST['tags']); + + $this->form_validation->reset_validation(); + $this->form_validation->set_data(array('tags' => array('a', 'b'))); + $this->form_validation->set_rules('tags', 'Tags', 'is_array'); + $data_processed = NULL; + + $this->assertTrue($this->form_validation->run('', $data_processed)); + $this->assertEquals(array('a', 'b'), $data_processed['tags']); + + $_POST = array(); + } + + public function test_array_input_without_is_array_rule() + { + $_POST = array('name' => array('x'), 'other' => 'y'); + $this->form_validation->set_rules('name', 'Name', 'alpha'); + $this->form_validation->set_rules('other', 'Other', 'required'); + + $this->assertTrue($this->form_validation->run()); + $this->assertNull($_POST['name']); + + $this->assertFalse($this->run_rules( + array(array('field' => 'ids[]', 'label' => 'IDs', 'rules' => 'numeric')), + array('ids' => array('1', 'x')) + )); + + $this->form_validation->reset_validation(); + $_POST = array('names' => array(' a ', ' b ')); + $this->form_validation->set_rules('names[]', 'Names', 'trim|required'); + + $this->assertTrue($this->form_validation->run()); + $this->assertEquals(array('a', 'b'), $_POST['names']); + + $_POST = array(); + } + /** * Run rules * diff --git a/tests/codeigniter/libraries/Useragent_test.php b/tests/codeigniter/libraries/Useragent_test.php index 8c5b28b9f7c..7ffe9fc8dc0 100644 --- a/tests/codeigniter/libraries/Useragent_test.php +++ b/tests/codeigniter/libraries/Useragent_test.php @@ -142,4 +142,11 @@ public function test_edge() $this->assertEquals('Edge', $this->agent->browser()); } + public function test_vivaldi() + { + $this->agent->parse('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Vivaldi/6.5.3206.48'); + $this->assertEquals('Vivaldi', $this->agent->browser()); + $this->assertEquals('6.5.3206.48', $this->agent->version()); + } + } \ No newline at end of file diff --git a/user_guide_src/source/installation/upgrade_320.rst b/user_guide_src/source/installation/upgrade_320.rst index 2226d8588ca..170acc01274 100644 --- a/user_guide_src/source/installation/upgrade_320.rst +++ b/user_guide_src/source/installation/upgrade_320.rst @@ -270,3 +270,62 @@ entries by key needs review. .. note:: In 3.2-dev, this function was broken and returned entries holding only a ``relative_path`` key, so there is nothing to migrate if you are coming from there. + +Step 16: Replace config/user_agents.php and config/mimes.php +============================================================ + +These config files have received some updates. Please copy them to +*application/config/user_agents.php* and *application/config/mimes.php*. + +Step 17: Check form validation rules for array fields +===================================================== + +This only affects form fields that receive arrays. If you don't validate +array input, you can skip this step. + +If you are upgrading from 3.1.x, check validation rules for field names +without brackets, such as ``tags``. Submitted arrays are now set to +``NULL`` in ``$_POST`` after a successful ``run()``; 3.1.x kept them in +``$_POST``. Add ``is_array`` to accept and keep the array, or use ``tags[]`` +to validate each element. + +If you are upgrading from 3.2-dev, check rules that include ``is_array`` +for bracketed field names such as ``items[]``. Rules and callbacks now +receive the whole array; previously only the first element was validated +and passed to callbacks. See :ref:`using-arrays-as-field-names`. + +Step 18: Check full-page caching with a custom cache_path +========================================================= + +If you set ``$config['cache_path']`` without a trailing slash, such as +``/var/cache/ci``, cached pages are now served. Previously, cache files +were written but never read. This does not affect configurations where +``cache_path`` is empty or already ends with a slash. + +Before upgrading, check that pages calling ``$this->output->cache()`` do +not include per-user content, such as account details or CSRF tokens. +Cached output can be served to other visitors. See +:doc:`Web Page Caching <../general/caching>`. + +Step 19: Check CAPTCHA image handling +===================================== + +The :doc:`CAPTCHA Helper <../helpers/captcha_helper>` function +:php:func:`create_captcha()` now correctly removes expired images from +``img_path`` when it runs. It matches filenames consisting of a 10-digit +timestamp with an optional fractional part (``.`` or ``,`` separator), +followed by ``.png`` or ``.jpg`` (case-insensitive). Keep ``img_path`` +dedicated to CAPTCHA images: any matching file is deleted after +``expiration`` seconds (default 7200). Other files are left alone. + +If you are upgrading from 3.1.x, check code that assumes JPEG output. +Since 3.2-dev, ``create_captcha()`` always writes PNG files; 3.1.x used +JPEG when ``imagejpeg()`` was available. Replace filenames built from +``$cap['time']`` plus ``.jpg`` with the returned ``$cap['filename']``, +which contains the exact filename (``$cap['time']`` plus ``.png``). + +Omitting both ``img_path`` and ``img_url`` returns the image inline as a +``data:image/png;base64,...`` URI again, as described in the CAPTCHA helper +documentation. This mode was added in 3.2-dev in 2017, but a 2020 merge +accidentally disabled it, causing ``create_captcha()`` to return ``FALSE`` +instead. diff --git a/user_guide_src/source/libraries/form_validation.rst b/user_guide_src/source/libraries/form_validation.rst index 5f30817eb1e..cc329cda057 100644 --- a/user_guide_src/source/libraries/form_validation.rst +++ b/user_guide_src/source/libraries/form_validation.rst @@ -957,6 +957,21 @@ When you use a helper function you'll include the bracket as well:: +Without the ``is_array`` rule, rules for bracketed field names run once +per array element. With ``is_array``, every rule in the list, including +callbacks and callables, receives the whole array instead. This applies +to field names with or without brackets, such as ``tags``, ``tags[]`` +and ``user[roles]``:: + + $this->form_validation->set_rules('tags', 'Tags', 'required|is_array'); + +The ``required`` rule combined with ``is_array`` rejects an empty array. + +For field names without brackets, array input is otherwise treated as an +empty value. After a successful ``run()``, that value is set to ``NULL`` +in ``$_POST``, or in the output variable passed as the second parameter +to ``run()``. Adding ``is_array`` accepts and keeps the array. + ************** Rule Reference @@ -969,6 +984,8 @@ use: Rule Parameter Description Example ========================= ========== ============================================================================================= ======================= **required** No Returns FALSE if the form element is empty. +**is_array** No Returns FALSE if the form element is not an array. + See :ref:`using-arrays-as-field-names` for details on validating entire arrays. **matches** Yes Returns FALSE if the form element does not match the one in the parameter. matches[form_item] **regex_match** Yes Returns FALSE if the form element does not match the regular expression. regex_match[/regex/] **differs** Yes Returns FALSE if the form element does not differ from the one in the parameter. differs[form_item] @@ -1009,8 +1026,8 @@ Rule Parameter Description **valid_base64** No Returns FALSE if the supplied string contains anything other than valid Base64 characters. ========================= ========== ============================================================================================= ======================= -.. note:: These rules can also be called as discrete methods. For - example:: +.. note:: These rules can also be called as discrete methods, except for + ``is_array``, which is a native PHP function. For example:: $this->form_validation->required($string);