Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/changelog.270.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Bug fixes:
- LDAP and Active Directory logins on PHP 8.3+ no longer emit a deprecation: ldap_connect() receives one ldap:// URI built from the configured server and port, a bare IPv6 address is bracketed, a server already written with an ldap:// or ldaps:// scheme is used as is, and STARTTLS is not attempted on an ldaps:// connection (class/auth/auth_ldap.php, class/auth/auth_ads.php)
- a cache engine without read() or write() throws LogicException instead of trigger_error(E_USER_ERROR), which PHP 8.4 deprecates (class/cache/xoopscache.php)
- xoops_aw_encode() and xoops_aw_decode() take their element by reference, so the array_walk() calls that encode and decode XOBJ_DTYPE_UNICODE_ARRAY values act on the array; write.php and XoopsObject::setVar() serialize the walked array instead of array_walk()'s boolean, and XoopsObject::getVar() returns the decoded array instead of true (include/functions.encoding.php, class/model/write.php, kernel/object.php)
- a XOBJ_DTYPE_UNICODE_ARRAY value reloads as the array that was stored: assignVar() keeps the serialized string as is instead of url-decoding it, which broke the string lengths of elements such as café or a+b and made getVar() return [], and XoopsModelWrite::cleanVars() no longer addslashes() the elements before url-encoding them, which stored the backslashes (kernel/object.php, class/model/write.php, tests/unit/htdocs/kernel/XoopsObjectUnicodeArrayTest.php)
- xoops_getUrlDomain() checks the host key only when parse_url() returned an array (include/functions.php)
- templates can use |sprintf without a Smarty 4 deprecation notice: XoopsTpl registers sprintf as a modifier, as it already does for strstr, trim and file_exists (class/template.php) (#264)

Expand Down
5 changes: 1 addition & 4 deletions htdocs/class/model/write.php
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,7 @@ public function cleanVars($object)
if (!$v['not_gpc']) {
$cleanv = array_map([&$myts, 'stripSlashesGPC'], $cleanv);
}
foreach (array_keys($cleanv) as $key) {
$cleanv[$key] = str_replace('\\"', '"', addslashes($cleanv[$key]));
}
// TODO: Not encoding safe, should try base64_encode -- phppp
// Elements are url-encoded, then the whole value is SQL-quoted below: no addslashes() pass.
array_walk($cleanv, 'xoops_aw_encode');
$cleanv = $this->handler->db->quote(serialize($cleanv));
break;
Expand Down
4 changes: 3 additions & 1 deletion htdocs/kernel/object.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,9 @@ public function assignVar($key, $value)
$value = $temp;
$this->vars[$key]['value'] = $value;
} else {
$this->vars[$key]['value'] = xoops_convert_decode($value);
// A stored value is a serialized array of encoded elements; getVar() unserializes
// and decodes it. Decoding the serialized string here would break its lengths.
$this->vars[$key]['value'] = $value;
Comment thread
mambax7 marked this conversation as resolved.
}
break;
case /** @scrutinizer ignore-deprecated */ XOBJ_DTYPE_UNICODE_URL:
Expand Down
119 changes: 119 additions & 0 deletions tests/unit/htdocs/kernel/XoopsObjectUnicodeArrayTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php
/*
* You may not change or alter any portion of this comment or credits
* of supporting developers from this source code or any supporting source code
* which is considered copyrighted (c) material of the original comment or credit authors.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*/

declare(strict_types=1);

namespace Tests\Unit\Htdocs\Kernel;

use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;

/**
* XOBJ_DTYPE_UNICODE_ARRAY must survive the write path and a reload.
*
* Runs in a PHP subprocess so the real include/functions.encoding.php is
* used: the suite bootstrap stubs xoops_convert_encode() as identity.
*
* @category Xoops
* @package Kernel
* @author XOOPS Development Team
* @copyright (c) 2000-2026 XOOPS Project (https://xoops.org)
* @license GNU GPL 2.0 or later (https://www.gnu.org/licenses/gpl-2.0.html)
* @link https://xoops.org
*/
#[CoversNothing]
final class XoopsObjectUnicodeArrayTest extends TestCase
{
#[Test]
public function unicodeArrayRoundTripsThroughBothWritePathsAndAssignVar(): void
{
$root = str_replace('\\', '/', XOOPS_ROOT_PATH);
$directory = sys_get_temp_dir() . '/xoops-unicode-array-' . bin2hex(random_bytes(4));
mkdir($directory);
$script = <<<PHP
<?php
define('XOOPS_ROOT_PATH', '{$root}');
define('XOOPS_PROT', 'http://');
define('_XOBJ_ERR_REQUIRED', '%s is required');
define('_XOBJ_ERR_SHORTERTHAN', '%s must be shorter than %d characters');
class MyTextSanitizer
{
public static function getInstance() { return new self(); }
public function stripSlashesGPC(\$text) { return \$text; }
public function censorString(\$text) { return \$text; }
}
require XOOPS_ROOT_PATH . '/include/functions.encoding.php';
require XOOPS_ROOT_PATH . '/kernel/object.php';
require XOOPS_ROOT_PATH . '/class/model/xoopsmodel.php';
require XOOPS_ROOT_PATH . '/class/model/write.php';

\$values = ['café', 'a+b', "O'Reilly", 'back\\\\slash', '100%'];
\$object = new class () extends XoopsObject {
public function __construct()
{
parent::__construct();
\$this->initVar('tags', XOBJ_DTYPE_UNICODE_ARRAY, []);
}
};
\$object->setVar('tags', \$values);

// XoopsObject::cleanVars(): what the legacy handlers store.
\$object->cleanVars();
\$storedByObject = \$object->cleanVars['tags'];

// XoopsModelWrite::cleanVars(): what XoopsPersistableObjectHandler stores, SQL-quoted.
\$db = new class () {
public function quote(\$v)
{
return "'" . addslashes((string) \$v) . "'";
}
};
\$write = new XoopsModelWrite();
\$write->handler = (object) ['db' => \$db];
\$write->cleanVars(\$object);
\$storedByModel = stripslashes(substr(\$object->cleanVars['tags'], 1, -1));

\$r = ['object' => \$storedByObject, 'model' => \$storedByModel, 'loaded' => []];
foreach (['object' => \$storedByObject, 'model' => \$storedByModel] as \$path => \$column) {
\$loaded = clone \$object;
\$loaded->assignVar('tags', \$column); // what the handler does on a database read
\$r['loaded'][\$path] = @\$loaded->getVar('tags');
}
echo json_encode(\$r);
PHP;
file_put_contents($directory . '/run.php', $script);
try {
$process = proc_open(
[PHP_BINARY, '-d', 'output_buffering=0', $directory . '/run.php'],
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes
);
self::assertIsResource($process);
$output = stream_get_contents($pipes[1]);
$errors = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
self::assertSame(0, proc_close($process), $errors . $output);
$r = json_decode((string) $output, true, 512, JSON_THROW_ON_ERROR);
} finally {
foreach (glob($directory . '/*') ?: [] as $file) {
unlink($file);
}
rmdir($directory);
}

$values = ['café', 'a+b', "O'Reilly", 'back\\slash', '100%'];
self::assertSame($r['object'], $r['model'], 'both write paths store the same serialized value');
self::assertSame($values, $r['loaded']['object'], 'XoopsObject::cleanVars() value reloads intact');
self::assertSame($values, $r['loaded']['model'], 'XoopsModelWrite::cleanVars() value reloads intact');
}
}