Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions system/DataCaster/Cast/EncryptedJsonCast.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

namespace CodeIgniter\DataCaster\Cast;

use SensitiveParameter;
use stdClass;

/**
* Class EncryptedJsonCast
*
* (PHP) [array|stdClass --> encrypted JSON string] --> (DB driver) --> (DB column) string
* [ <-- decrypted JSON string] <-- (DB driver) <-- (DB column) encrypted JSON string
*/
class EncryptedJsonCast extends BaseCast
{
/**
* @return array<array-key, mixed>|stdClass|null
*/
public static function get(
#[SensitiveParameter]
mixed $value,
array $params = [],
?object $helper = null,
): array|stdClass|null {
if ($value === null) {
return null;
}

$json = EncryptedCast::get($value, $params, $helper);

return JsonCast::get($json, $params, $helper);
}

public static function set(
#[SensitiveParameter]
mixed $value,
array $params = [],
?object $helper = null,
): ?string {
if ($value === null) {
return null;
}

$json = JsonCast::set($value, $params, $helper);

return EncryptedCast::set($json, $params, $helper);
}
}
42 changes: 24 additions & 18 deletions system/DataCaster/DataCaster.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use CodeIgniter\DataCaster\Cast\CSVCast;
use CodeIgniter\DataCaster\Cast\DatetimeCast;
use CodeIgniter\DataCaster\Cast\EncryptedCast;
use CodeIgniter\DataCaster\Cast\EncryptedJsonCast;
use CodeIgniter\DataCaster\Cast\EnumCast;
use CodeIgniter\DataCaster\Cast\FloatCast;
use CodeIgniter\DataCaster\Cast\IntBoolCast;
Expand Down Expand Up @@ -51,21 +52,22 @@ final class DataCaster
* @var cast_handlers [type => classname]
*/
private array $castHandlers = [
'array' => ArrayCast::class,
'bool' => BooleanCast::class,
'boolean' => BooleanCast::class,
'csv' => CSVCast::class,
'datetime' => DatetimeCast::class,
'encrypted' => EncryptedCast::class,
'enum' => EnumCast::class,
'double' => FloatCast::class,
'float' => FloatCast::class,
'int' => IntegerCast::class,
'integer' => IntegerCast::class,
'int-bool' => IntBoolCast::class,
'json' => JsonCast::class,
'timestamp' => TimestampCast::class,
'uri' => URICast::class,
'array' => ArrayCast::class,
'bool' => BooleanCast::class,
'boolean' => BooleanCast::class,
'csv' => CSVCast::class,
'datetime' => DatetimeCast::class,
'encrypted' => EncryptedCast::class,
'encrypted-json' => EncryptedJsonCast::class,
'enum' => EnumCast::class,
'double' => FloatCast::class,
'float' => FloatCast::class,
'int' => IntegerCast::class,
'integer' => IntegerCast::class,
'int-bool' => IntBoolCast::class,
'json' => JsonCast::class,
'timestamp' => TimestampCast::class,
'uri' => URICast::class,
];

/**
Expand Down Expand Up @@ -159,9 +161,13 @@ public function castAs(mixed $value, string $field, string $method = 'get'): mix
}
}

// In order not to create a separate handler for the
// json-array type, we transform the required one.
$type = ($type === 'json-array') ? 'json[array]' : $type;
// In order not to create separate handlers for these
// array variants, we transform the required ones.
$type = match ($type) {
'json-array' => 'json[array]',
'encrypted-json-array' => 'encrypted-json[array]',
default => $type,
};

$params = [];

Expand Down
104 changes: 104 additions & 0 deletions tests/system/DataCaster/EncryptedCastTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use Config\Services;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
use stdClass;

/**
* @internal
Expand Down Expand Up @@ -109,6 +110,73 @@ public function testEncryptedCastCanDecryptPreviousKeyValues(): void
$this->assertSame('old-secret', $dataCaster->castAs($oldEncryptedValue, 'secret'));
}

public function testEncryptedJsonCastConvertsObjectToAndFromEncryptedJson(): void
{
$dataCaster = new DataCaster(types: ['settings' => 'encrypted-json']);

$encrypted = $dataCaster->castAs((object) ['notifications' => true, 'theme' => 'dark'], 'settings', 'set');

$this->assertIsString($encrypted);
$this->assertNotSame('{"notifications":true,"theme":"dark"}', $encrypted);
$this->assertNotFalse(base64_decode($encrypted, true));

$settings = $dataCaster->castAs($encrypted, 'settings');

$this->assertInstanceOf(stdClass::class, $settings);
$this->assertTrue($settings->notifications);
$this->assertSame('dark', $settings->theme);
}

public function testEncryptedJsonArrayCastConvertsArrayToAndFromEncryptedJson(): void
{
$dataCaster = new DataCaster(types: ['settings' => 'encrypted-json-array']);

$encrypted = $dataCaster->castAs(['notifications' => true, 'theme' => 'dark'], 'settings', 'set');

$this->assertIsString($encrypted);
$this->assertNotSame('{"notifications":true,"theme":"dark"}', $encrypted);
$this->assertSame(
['notifications' => true, 'theme' => 'dark'],
$dataCaster->castAs($encrypted, 'settings'),
);
}

public function testEncryptedJsonCastsSupportNullableValues(): void
{
$dataCaster = new DataCaster(types: [
'settings' => '?encrypted-json',
'arraySettings' => '?encrypted-json-array',
]);

$this->assertNull($dataCaster->castAs(null, 'settings', 'set'));
$this->assertNull($dataCaster->castAs(null, 'settings'));
$this->assertNull($dataCaster->castAs(null, 'arraySettings', 'set'));
$this->assertNull($dataCaster->castAs(null, 'arraySettings'));
}

public function testEncryptedJsonCastRejectsMalformedEncryptedPayload(): void
{
$this->expectException(CastException::class);
$this->expectExceptionMessage('Type casting "encrypted" expects a valid encrypted value.');

$dataCaster = new DataCaster(types: ['settings' => 'encrypted-json']);

$dataCaster->castAs('@@not-base64@@', 'settings');
}

public function testEncryptedJsonCastRejectsDecryptedMalformedJson(): void
{
$this->expectException(CastException::class);
$this->expectExceptionMessage('Syntax error, malformed JSON.');

$encryptedCast = new DataCaster(types: ['settings' => 'encrypted']);
$encrypted = $encryptedCast->castAs('not-json', 'settings', 'set');

$dataCaster = new DataCaster(types: ['settings' => 'encrypted-json']);

$dataCaster->castAs($encrypted, 'settings');
}

public function testDataConverterConvertsEncryptedFieldToAndFromDataSource(): void
{
$converter = new DataConverter(['secret' => 'encrypted']);
Expand All @@ -120,6 +188,21 @@ public function testDataConverterConvertsEncryptedFieldToAndFromDataSource(): vo
$this->assertSame(['secret' => 'plain-secret'], $converter->fromDataSource($dataSourceData));
}

public function testDataConverterConvertsEncryptedJsonArrayFieldToAndFromDataSource(): void
{
$converter = new DataConverter(['settings' => 'encrypted-json-array']);

$dataSourceData = $converter->toDataSource([
'settings' => ['notifications' => true, 'theme' => 'dark'],
]);

$this->assertIsString($dataSourceData['settings']);
$this->assertSame(
['settings' => ['notifications' => true, 'theme' => 'dark']],
$converter->fromDataSource($dataSourceData),
);
}

public function testEntityStoresEncryptedRawValueAndReturnsPlaintext(): void
{
$entity = new class () extends Entity {
Expand All @@ -138,6 +221,27 @@ public function testEntityStoresEncryptedRawValueAndReturnsPlaintext(): void
$this->assertSame(['secret' => 'plain-secret'], $entity->toArray());
}

public function testEntityStoresEncryptedJsonRawValueAndReturnsPlainValue(): void
{
$entity = new class () extends Entity {
protected $casts = [
'settings' => 'encrypted-json-array',
];
};

$entity->settings = ['notifications' => true, 'theme' => 'dark'];

$raw = $entity->toRawArray();

$this->assertIsString($raw['settings']);
$this->assertNotSame('{"notifications":true,"theme":"dark"}', $raw['settings']);
$this->assertSame(['notifications' => true, 'theme' => 'dark'], $entity->settings);
$this->assertSame(
['settings' => ['notifications' => true, 'theme' => 'dark']],
$entity->toArray(),
);
}

/**
* @param list<string> $previousKeys
*/
Expand Down
6 changes: 3 additions & 3 deletions user_guide_src/source/changelogs/v4.8.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ Model
=====

- Added new ``chunkRows()`` method to ``CodeIgniter\Model`` for processing large datasets in smaller chunks.
- Added ``encrypted`` casting for Entity properties and Model fields using the Encryption service. See :ref:`model-field-casting-encrypted`.
- Added ``encrypted``, ``encrypted-json`` and ``encrypted-json-array`` casting for Entity properties and Model fields using the Encryption service. See :ref:`model-field-casting-encrypted`.
- Added new ``firstOrInsert()`` method to ``CodeIgniter\Model`` that finds the first row matching the given attributes or inserts a new one. See :ref:`model-first-or-insert`.
- Added ``$insertOnlyFields`` and ``setInsertOnlyFields()`` to ``CodeIgniter\Model`` to remove configured fields from Model update operations while allowing them during inserts. See :ref:`model-insert-only-fields`.
- Added ``$throwOnDisallowedFields`` and ``throwOnDisallowedFields()`` to ``CodeIgniter\Model`` to throw a ``DataException`` when write data contains fields that would otherwise be discarded by ``$allowedFields``. See :ref:`model-throw-on-disallowed-fields`.
Expand All @@ -300,8 +300,8 @@ Helpers and Functions
object rows (including ``Entity``) in :php:func:`dot_array_search()`,
:php:func:`dot_array_has()`, :php:func:`dot_array_only()`,
:php:func:`dot_array_except()`, and :php:func:`array_group_by()`.
:php:func:`dot_array_only()` and :php:func:`dot_array_except()` still return arrays. If the source itself is an object,
it is read as an array-like value. Object values inside the source are kept unchanged when selected as a whole or left untouched.
:php:func:`dot_array_only()` and :php:func:`dot_array_except()` still return arrays. If the source itself is an object,
it is read as an array-like value. Object values inside the source are kept unchanged when selected as a whole or left untouched.
Partial object paths are returned as arrays.

HTTP
Expand Down
10 changes: 8 additions & 2 deletions user_guide_src/source/models/entities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -256,12 +256,12 @@ Scalar Type Casting
-------------------

Properties can be cast to any of the following data types:
**integer**, **float**, **double**, **string**, **boolean**, **object**, **array**, **datetime**, **timestamp**, **uri**, **int-bool**, **enum** and **encrypted**.
**integer**, **float**, **double**, **string**, **boolean**, **object**, **array**, **datetime**, **timestamp**, **uri**, **int-bool**, **enum**, **encrypted**, **encrypted-json** and **encrypted-json-array**.
Add a question mark at the beginning of type to mark property as nullable, i.e., **?string**, **?integer**.

.. note:: **int-bool** can be used since v4.3.0.
.. note:: **enum** can be used since v4.7.0.
.. note:: **encrypted** can be used since v4.8.0.
.. note:: **encrypted**, **encrypted-json** and **encrypted-json-array** can be used since v4.8.0.
.. note:: Since v4.8.0, you can also pass parameters to **float** and **double** types to specify the number of decimal places and rounding mode, i.e., **float[2,even]**.

For example, if you had a User entity with an ``is_banned`` property, you can cast it as a boolean:
Expand Down Expand Up @@ -349,6 +349,12 @@ for nullable values.

.. literalinclude:: entities/029.php

The ``encrypted-json`` and ``encrypted-json-array`` types JSON encode values
before encryption and JSON decode them after decryption. The ``encrypted-json``
type returns ``stdClass`` values, while ``encrypted-json-array`` returns arrays.
Use ``?encrypted-json`` and ``?encrypted-json-array`` for nullable values. These
values are still stored as encrypted text, not as queryable database JSON.

Encrypted values are stored as Base64-encoded ciphertext. Use a ``TEXT`` column
or a sufficiently large string column because the stored value is longer than
the plain text value. Avoid narrow columns like ``VARCHAR(255)`` unless you have
Expand Down
70 changes: 40 additions & 30 deletions user_guide_src/source/models/model.rst
Original file line number Diff line number Diff line change
Expand Up @@ -399,35 +399,39 @@ Data Types
The following types are provided by default. Add a question mark at the beginning
of type to mark the field as nullable, i.e., ``?int``, ``?datetime``.

+---------------+----------------+---------------------------+
| Type | PHP Value Type | DB Column Type |
+===============+================+===========================+
|``int`` | int | int type |
+---------------+----------------+---------------------------+
|``float`` | float | float (numeric) type |
+---------------+----------------+---------------------------+
|``bool`` | bool | bool/int/string type |
+---------------+----------------+---------------------------+
|``int-bool`` | bool | int type (1 or 0) |
+---------------+----------------+---------------------------+
|``array`` | array | string type (serialized) |
+---------------+----------------+---------------------------+
|``csv`` | array | string type (CSV) |
+---------------+----------------+---------------------------+
|``json`` | stdClass | json/string type |
+---------------+----------------+---------------------------+
|``json-array`` | array | json/string type |
+---------------+----------------+---------------------------+
|``datetime`` | Time | datetime type |
+---------------+----------------+---------------------------+
|``timestamp`` | Time | int type (UNIX timestamp) |
+---------------+----------------+---------------------------+
|``uri`` | URI | string type |
+---------------+----------------+---------------------------+
|``enum`` | Enum | string/int type |
+---------------+----------------+---------------------------+
|``encrypted`` | string | string/text type |
+---------------+----------------+---------------------------+
+--------------------------+----------------+---------------------------+
| Type | PHP Value Type | DB Column Type |
+==========================+================+===========================+
|``int`` | int | int type |
+--------------------------+----------------+---------------------------+
|``float`` | float | float (numeric) type |
+--------------------------+----------------+---------------------------+
|``bool`` | bool | bool/int/string type |
+--------------------------+----------------+---------------------------+
|``int-bool`` | bool | int type (1 or 0) |
+--------------------------+----------------+---------------------------+
|``array`` | array | string type (serialized) |
+--------------------------+----------------+---------------------------+
|``csv`` | array | string type (CSV) |
+--------------------------+----------------+---------------------------+
|``json`` | stdClass | json/string type |
+--------------------------+----------------+---------------------------+
|``json-array`` | array | json/string type |
+--------------------------+----------------+---------------------------+
|``datetime`` | Time | datetime type |
+--------------------------+----------------+---------------------------+
|``timestamp`` | Time | int type (UNIX timestamp) |
+--------------------------+----------------+---------------------------+
|``uri`` | URI | string type |
+--------------------------+----------------+---------------------------+
|``enum`` | Enum | string/int type |
+--------------------------+----------------+---------------------------+
|``encrypted`` | string | string/text type |
+--------------------------+----------------+---------------------------+
|``encrypted-json`` | stdClass | string/text type |
+--------------------------+----------------+---------------------------+
|``encrypted-json-array`` | array | string/text type |
+--------------------------+----------------+---------------------------+

float
-----
Expand Down Expand Up @@ -505,7 +509,13 @@ encryption key before using it. The configured key is required for both writing
new values and reading stored values back. The ``encrypted`` type accepts string
values. Use ``?encrypted`` for nullable values.

.. literalinclude:: model/069.php
.. literalinclude:: model/070.php

The ``encrypted-json`` and ``encrypted-json-array`` types JSON encode values
before encryption and JSON decode them after decryption. The ``encrypted-json``
type returns ``stdClass`` values, while ``encrypted-json-array`` returns arrays.
Use ``?encrypted-json`` and ``?encrypted-json-array`` for nullable values. These
values are still stored as encrypted text, not as queryable database JSON.

Encrypted values are stored as Base64-encoded ciphertext. Use a ``TEXT`` column
or a sufficiently large string column because the stored value is longer than
Expand Down
Loading