Skip to content

Commit 576a267

Browse files
authored
Add Security\Serializer: secure serialization toolkit with JSON, PHP, and legacy format support (#121)
Serializer
2 parents 7e5949d + 36d9d5b commit 576a267

File tree

5 files changed

+1113
-0
lines changed

5 files changed

+1113
-0
lines changed

phpstan.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ parameters:
1010
excludePaths:
1111
- tests/
1212
treatPhpDocTypesAsCertain: false
13+
ignoreErrors:
14+
-
15+
message: '#Trait .+ is used zero times and is not analysed\.#'
16+
path: src/Security/SerializableTrait.php

src/Security/Format.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
/*
3+
You may not change or alter any portion of this comment or credits
4+
of supporting developers from this source code or any supporting source code
5+
which is considered copyrighted (c) material of the original comment or credit authors.
6+
7+
This program is distributed in the hope that it will be useful,
8+
but WITHOUT ANY WARRANTY; without even the implied warranty of
9+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Xmf\Security;
15+
16+
/**
17+
* Format types for serialization (PHP 7.4+ compatible)
18+
*
19+
* @category Xmf\Security
20+
* @package Xmf
21+
* @author MAMBA <mambax7@gmail.com>
22+
* @copyright 2000-2025 XOOPS Project (https://xoops.org)
23+
* @license GNU GPL 2.0 or later (https://www.gnu.org/licenses/gpl-2.0.html)
24+
* @link https://xoops.org
25+
*/
26+
final class Format
27+
{
28+
public const JSON = 'json';
29+
public const PHP = 'php';
30+
public const LEGACY = 'legacy'; // base64-encoded PHP serialize
31+
public const AUTO = 'auto'; // auto-detect format
32+
33+
/**
34+
* Prevent instantiation
35+
*/
36+
private function __construct()
37+
{
38+
}
39+
}

src/Security/SerializableTrait.php

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
/*
3+
You may not change or alter any portion of this comment or credits
4+
of supporting developers from this source code or any supporting source code
5+
which is considered copyrighted (c) material of the original comment or credit authors.
6+
7+
This program is distributed in the hope that it will be useful,
8+
but WITHOUT ANY WARRANTY; without even the implied warranty of
9+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Xmf\Security;
15+
16+
/**
17+
* Trait providing serialization utilities for object properties
18+
*
19+
* @method void setVar(string $key, mixed $value)
20+
*
21+
* @category Xmf\Security
22+
* @package Xmf
23+
* @author MAMBA <mambax7@gmail.com>
24+
* @copyright 2000-2025 XOOPS Project (https://xoops.org)
25+
* @license GNU GPL 2.0 or later (https://www.gnu.org/licenses/gpl-2.0.html)
26+
* @link https://xoops.org
27+
*/
28+
trait SerializableTrait
29+
{
30+
/**
31+
* Serialize a property for database storage
32+
*
33+
* Uses Serializer::toJson() for Format::JSON or Serializer::toPhp()
34+
* for Format::PHP. Other formats are not supported for serialization.
35+
*
36+
* @param mixed $value The value to serialize
37+
* @param string $format The target format (Format::JSON or Format::PHP)
38+
*
39+
* @return string The serialized representation
40+
*
41+
* @throws \InvalidArgumentException On unsupported format
42+
*/
43+
protected function serializeProperty($value, string $format = Format::JSON): string
44+
{
45+
if ($format === Format::JSON) {
46+
return Serializer::toJson($value);
47+
}
48+
if ($format === Format::PHP) {
49+
return Serializer::toPhp($value);
50+
}
51+
52+
throw new \InvalidArgumentException(
53+
sprintf('Unsupported serialization format "%s"', $format)
54+
);
55+
}
56+
57+
/**
58+
* Deserialize a property from database
59+
*
60+
* Uses Serializer::tryFrom() with Format::AUTO to auto-detect format.
61+
*
62+
* @param string $data The serialized data
63+
* @param mixed $default Default value if deserialization fails
64+
* @param array<int, string> $allowedClasses Whitelist of allowed class names
65+
*
66+
* @return mixed The deserialized value or default
67+
*/
68+
protected function unserializeProperty(string $data, $default = null, array $allowedClasses = [])
69+
{
70+
return Serializer::tryFrom($data, $default, Format::AUTO, $allowedClasses);
71+
}
72+
73+
/**
74+
* Serialize all marked properties
75+
*
76+
* Iterates getSerializableProperties() and uses serializeProperty()
77+
* to produce an associative array of serialized values.
78+
*
79+
* @return array<string, string> Map of property names to serialized values
80+
*
81+
* @throws \InvalidArgumentException On unsupported format in serializeProperty()
82+
*/
83+
public function serializeProperties(): array
84+
{
85+
$serialized = [];
86+
87+
foreach ($this->getSerializableProperties() as $property => $format) {
88+
if (property_exists($this, $property)) {
89+
try {
90+
$serialized[$property] = $this->serializeProperty($this->$property, $format);
91+
} catch (\Error $e) {
92+
// Typed property may not be initialized — skip it
93+
}
94+
}
95+
}
96+
97+
return $serialized;
98+
}
99+
100+
/**
101+
* Define which properties should be serialized
102+
*
103+
* Override in your class to specify properties and their formats.
104+
* Example: return ['forum_moderators' => Format::JSON];
105+
*
106+
* @return array<string, string> Map of property name to Format constant
107+
*/
108+
protected function getSerializableProperties(): array
109+
{
110+
return [
111+
// 'property_name' => Format::JSON
112+
];
113+
}
114+
115+
/**
116+
* Migrate serialized data from old to new format
117+
*
118+
* Requires the using class to implement setVar() (e.g. XoopsObject).
119+
* Migration is automatically tracked via Serializer::setLegacyLogger()
120+
* when the legacy logger is configured.
121+
*
122+
* @param string $property Property name to migrate
123+
* @param string $oldData Current serialized data
124+
*
125+
* @return bool True if migration was performed
126+
*/
127+
public function migrateSerializedData(string $property, string $oldData): bool
128+
{
129+
$format = Serializer::detect($oldData);
130+
131+
if ($format === Format::PHP || $format === Format::LEGACY) {
132+
try {
133+
$value = $this->unserializeProperty($oldData);
134+
$newData = $this->serializeProperty($value, Format::JSON);
135+
$this->setVar($property, $newData);
136+
137+
return true;
138+
} catch (\Throwable $e) {
139+
\error_log(\sprintf(
140+
'Serializer migration failed for property "%s": %s',
141+
$property,
142+
$e->getMessage()
143+
));
144+
145+
return false;
146+
}
147+
}
148+
149+
return false;
150+
}
151+
}

0 commit comments

Comments
 (0)