diff --git a/composer.json b/composer.json index 294c5fc18..84ed3c595 100644 --- a/composer.json +++ b/composer.json @@ -117,5 +117,6 @@ "branch-alias": { "dev-FRAMEWORK_6_0": "7.x-dev" } - } -} \ No newline at end of file + }, + "minimum-stability": "dev" +} diff --git a/lib/Ajax/Imple/ImportEncryptKey.php b/lib/Ajax/Imple/ImportEncryptKey.php index 0bb18c25d..4b7c36e67 100644 --- a/lib/Ajax/Imple/ImportEncryptKey.php +++ b/lib/Ajax/Imple/ImportEncryptKey.php @@ -10,6 +10,8 @@ * @copyright 2012-2017 Horde LLC * @license http://www.horde.org/licenses/gpl GPL * @package IMP + +use Horde\Util\Variables; */ /** @@ -20,6 +22,8 @@ * @copyright 2012-2017 Horde LLC * @license http://www.horde.org/licenses/gpl GPL * @package IMP + +use Horde\Util\Variables; */ class IMP_Ajax_Imple_ImportEncryptKey extends Horde_Core_Ajax_Imple { @@ -58,7 +62,7 @@ protected function _attach($init) * @return boolean True on success. * @throws IMP_Exception */ - protected function _handle(Horde_Variables $vars) + protected function _handle(Variables|Horde_Variables $vars) { global $injector, $notification; diff --git a/lib/Ajax/Imple/ItipRequest.php b/lib/Ajax/Imple/ItipRequest.php index 6586226f9..914f55a54 100644 --- a/lib/Ajax/Imple/ItipRequest.php +++ b/lib/Ajax/Imple/ItipRequest.php @@ -10,6 +10,8 @@ * @copyright 2012-2017 Horde LLC * @license http://www.horde.org/licenses/gpl GPL * @package IMP + +use Horde\Util\Variables; */ /** @@ -20,6 +22,8 @@ * @copyright 2012-2017 Horde LLC * @license http://www.horde.org/licenses/gpl GPL * @package IMP + +use Horde\Util\Variables; */ class IMP_Ajax_Imple_ItipRequest extends Horde_Core_Ajax_Imple { @@ -57,7 +61,7 @@ protected function _attach($init) * * @return boolean True on success. */ - protected function _handle(Horde_Variables $vars) + protected function _handle(Variables|Horde_Variables $vars) { global $injector, $notification, $registry; diff --git a/lib/Ajax/Imple/PassphraseDialog.php b/lib/Ajax/Imple/PassphraseDialog.php index 2d0de88ac..6975b4227 100644 --- a/lib/Ajax/Imple/PassphraseDialog.php +++ b/lib/Ajax/Imple/PassphraseDialog.php @@ -10,6 +10,8 @@ * @copyright 2010-2017 Horde LLC * @license http://www.horde.org/licenses/gpl GPL * @package IMP + +use Horde\Util\Variables; */ /** @@ -20,6 +22,8 @@ * @copyright 2010-2017 Horde LLC * @license http://www.horde.org/licenses/gpl GPL * @package IMP + +use Horde\Util\Variables; */ class IMP_Ajax_Imple_PassphraseDialog extends Horde_Core_Ajax_Imple { @@ -84,7 +88,7 @@ protected function _attach($init) /** */ - protected function _handle(Horde_Variables $vars) + protected function _handle(Variables|Horde_Variables $vars) { return false; } diff --git a/lib/Ajax/Imple/VcardImport.php b/lib/Ajax/Imple/VcardImport.php index 102e61bd2..f0bd2dd6e 100644 --- a/lib/Ajax/Imple/VcardImport.php +++ b/lib/Ajax/Imple/VcardImport.php @@ -10,6 +10,8 @@ * @copyright 2012-2017 Horde LLC * @license http://www.horde.org/licenses/gpl GPL * @package IMP + +use Horde\Util\Variables; */ /** @@ -20,6 +22,8 @@ * @copyright 2014-2017 Horde LLC * @license http://www.horde.org/licenses/gpl GPL * @package IMP + +use Horde\Util\Variables; */ class IMP_Ajax_Imple_VcardImport extends Horde_Core_Ajax_Imple { @@ -59,7 +63,7 @@ protected function _attach($init) * * @return boolean True on success. */ - protected function _handle(Horde_Variables $vars) + protected function _handle(Variables|Horde_Variables $vars) { global $registry, $injector, $notification; diff --git a/lib/Application.php b/lib/Application.php index 1317ad502..949e015b1 100644 --- a/lib/Application.php +++ b/lib/Application.php @@ -31,6 +31,8 @@ * Horde_Registry_Application::). */ require_once HORDE_BASE . '/lib/core.php'; +use Horde\Util\Variables; + /** * IMP application API. * @@ -411,7 +413,7 @@ public function nosqlDrivers() * * @throws IMP_Exception */ - public function download(Horde_Variables $vars) + public function download(Variables|Horde_Variables $vars) { global $injector, $registry; diff --git a/lib/Basic/Base.php b/lib/Basic/Base.php index e10387d5b..4e7d9f6d7 100644 --- a/lib/Basic/Base.php +++ b/lib/Basic/Base.php @@ -12,6 +12,8 @@ * @package IMP */ +use Horde\Util\Variables; + /** * Base class for basic view pages. * @@ -50,7 +52,7 @@ abstract class IMP_Basic_Base /** */ - public function __construct(Horde_Variables $vars) + public function __construct(Variables|Horde_Variables $vars) { global $page_output; diff --git a/lib/Compose.php b/lib/Compose.php index fe580123b..3bd2be85c 100644 --- a/lib/Compose.php +++ b/lib/Compose.php @@ -12,6 +12,7 @@ * @package IMP */ use function PHP81_BC\strftime; +use Horde\Util\Variables; /** * An object representing an outgoing mail message. @@ -3504,7 +3505,7 @@ protected function _addAttachment($atc_file, $bytes, $filename, $type) * * @param Horde_Variables $vars Object with the form data. */ - public function sessionExpireDraft(Horde_Variables $vars) + public function sessionExpireDraft(Variables|Horde_Variables $vars) { global $injector; diff --git a/lib/Compose/Link.php b/lib/Compose/Link.php index 9112264fe..bcad76a4c 100644 --- a/lib/Compose/Link.php +++ b/lib/Compose/Link.php @@ -12,6 +12,8 @@ * @package IMP */ +use Horde\Util\Variables; + /** * Process incoming compose arguments and generate compose links. * @@ -44,7 +46,7 @@ public function __construct($in = null) } else { $this->args['to'] = $in; } - } elseif ($in instanceof Horde_Variables) { + } elseif ($in instanceof Horde_Variables || $in instanceof Variables) { foreach ($fields as $val) { if (isset($in->$val)) { $this->args[$val] = $in->$val; diff --git a/lib/Contents/View.php b/lib/Contents/View.php index 58140182a..395b9d6be 100644 --- a/lib/Contents/View.php +++ b/lib/Contents/View.php @@ -12,6 +12,8 @@ * @package IMP */ +use Horde\Util\Variables; + /** * Provides logic to format message content for delivery to the browser. * @@ -340,7 +342,7 @@ public function printAttach($id) * * @throws Horde_Exception Exception on incorrect token. */ - public function checkToken(Horde_Variables $vars) + public function checkToken(Variables|Horde_Variables $vars) { $GLOBALS['session']->checkToken($vars->get(self::VIEW_TOKEN_PARAM)); } diff --git a/lib/Dynamic/Base.php b/lib/Dynamic/Base.php index 4f2edf000..9d5e0f035 100644 --- a/lib/Dynamic/Base.php +++ b/lib/Dynamic/Base.php @@ -12,6 +12,8 @@ * @package IMP */ +use Horde\Util\Variables; + /** * Base class for dynamic view pages. * @@ -67,7 +69,7 @@ abstract class IMP_Dynamic_Base /** */ - public function __construct(Horde_Variables $vars) + public function __construct(Variables|Horde_Variables $vars) { global $page_output; diff --git a/lib/Indices/Mailbox.php b/lib/Indices/Mailbox.php index f5b14d806..083c94a14 100644 --- a/lib/Indices/Mailbox.php +++ b/lib/Indices/Mailbox.php @@ -12,6 +12,8 @@ * @package IMP */ +use Horde\Util\Variables; + /** * Extends base Indices object by incorporating base mailbox information. * @@ -55,7 +57,7 @@ public function __construct() switch (func_num_args()) { case 1: - if ($args[0] instanceof Horde_Variables) { + if ($args[0] instanceof Horde_Variables || $args[0] instanceof Variables) { if (isset($args[0]->mailbox) && strlen($args[0]->mailbox)) { $this->mailbox = IMP_Mailbox::formFrom($args[0]->mailbox); diff --git a/lib/Smartmobile.php b/lib/Smartmobile.php index c4a26d6c0..115347a83 100644 --- a/lib/Smartmobile.php +++ b/lib/Smartmobile.php @@ -12,6 +12,8 @@ * @package IMP */ +use Horde\Util\Variables; + /** * Base class for smartmobile view pages. * @@ -35,7 +37,7 @@ class IMP_Smartmobile /** */ - public function __construct(Horde_Variables $vars) + public function __construct(Variables|Horde_Variables $vars) { global $notification, $page_output; diff --git a/test/Imp/Unit/VariablesCompatibilityTest.php b/test/Imp/Unit/VariablesCompatibilityTest.php new file mode 100644 index 000000000..b7bfaa623 --- /dev/null +++ b/test/Imp/Unit/VariablesCompatibilityTest.php @@ -0,0 +1,191 @@ +getParameters(); + + $this->assertCount(1, $params); + $this->assertEquals('vars', $params[0]->getName()); + + // Verify type accepts both + $type = $params[0]->getType(); + $this->assertInstanceOf(ReflectionUnionType::class, $type); + + $types = array_map(fn($t) => $t->getName(), $type->getTypes()); + $this->assertContains('Horde\Util\Variables', $types); + $this->assertContains('Horde_Variables', $types); + } + + /** + * Test IMP_Contents_View::checkToken accepts both types + */ + public function testContentsViewCheckTokenAcceptsBothTypes(): void + { + $method = new ReflectionMethod(IMP_Contents_View::class, 'checkToken'); + $params = $method->getParameters(); + + $this->assertCount(1, $params); + $this->assertEquals('vars', $params[0]->getName()); + + $type = $params[0]->getType(); + $this->assertInstanceOf(ReflectionUnionType::class, $type); + + $types = array_map(fn($t) => $t->getName(), $type->getTypes()); + $this->assertContains('Horde\Util\Variables', $types); + $this->assertContains('Horde_Variables', $types); + } + + /** + * Test IMP_Indices_Mailbox instanceof check works with both types + */ + public function testIndicesMailboxInstanceofWorksWithLegacy(): void + { + $vars = new Horde_Variables(['test' => 'value']); + + // Test the instanceof logic that's in IMP_Indices_Mailbox + $this->assertTrue( + $vars instanceof Horde_Variables || $vars instanceof Variables, + 'Legacy Horde_Variables should match instanceof check' + ); + } + + /** + * Test IMP_Indices_Mailbox instanceof check works with modern type + */ + public function testIndicesMailboxInstanceofWorksWithModern(): void + { + $vars = new Variables(['test' => 'value']); + + // Test the instanceof logic that's in IMP_Indices_Mailbox + $this->assertTrue( + $vars instanceof Horde_Variables || $vars instanceof Variables, + 'Modern Horde\Util\Variables should match instanceof check' + ); + } + + /** + * Test that both Variables types have compatible interfaces + */ + public function testBothTypesImplementSameInterfaces(): void + { + $legacy = new ReflectionClass(Horde_Variables::class); + $modern = new ReflectionClass(Variables::class); + + $legacyInterfaces = $legacy->getInterfaceNames(); + $modernInterfaces = $modern->getInterfaceNames(); + + // Both should implement ArrayAccess, Countable, IteratorAggregate + $requiredInterfaces = ['ArrayAccess', 'Countable', 'IteratorAggregate']; + + foreach ($requiredInterfaces as $interface) { + $this->assertContains($interface, $legacyInterfaces); + $this->assertContains($interface, $modernInterfaces); + } + } + + /** + * Test that both Variables types have compatible methods + */ + public function testBothTypesHaveCompatibleMethods(): void + { + $legacy = new ReflectionClass(Horde_Variables::class); + $modern = new ReflectionClass(Variables::class); + + $requiredMethods = ['get', 'exists', 'set', 'remove']; + + foreach ($requiredMethods as $methodName) { + $this->assertTrue( + $legacy->hasMethod($methodName), + "Horde_Variables should have method: $methodName" + ); + $this->assertTrue( + $modern->hasMethod($methodName), + "Horde\Util\Variables should have method: $methodName" + ); + } + } + + /** + * Test property access works identically on both types + */ + public function testPropertyAccessWorksOnBothTypes(): void + { + $legacyVars = new Horde_Variables(['foo' => 'bar', 'baz' => 'qux']); + $modernVars = new Variables(['foo' => 'bar', 'baz' => 'qux']); + + // Test dynamic property access + $this->assertEquals('bar', $legacyVars->foo); + $this->assertEquals('bar', $modernVars->foo); + + // Test get method + $this->assertEquals('qux', $legacyVars->get('baz')); + $this->assertEquals('qux', $modernVars->get('baz')); + + // Test default values + $this->assertEquals('default', $legacyVars->get('missing', 'default')); + $this->assertEquals('default', $modernVars->get('missing', 'default')); + } + + /** + * Test array access works identically on both types + */ + public function testArrayAccessWorksOnBothTypes(): void + { + $legacyVars = new Horde_Variables(['key' => 'value']); + $modernVars = new Variables(['key' => 'value']); + + // Test ArrayAccess interface + $this->assertEquals('value', $legacyVars['key']); + $this->assertEquals('value', $modernVars['key']); + + $this->assertTrue(isset($legacyVars['key'])); + $this->assertTrue(isset($modernVars['key'])); + + $this->assertFalse(isset($legacyVars['missing'])); + $this->assertFalse(isset($modernVars['missing'])); + } +} diff --git a/test/css-filter-test.php b/test/css-filter-test.php new file mode 100755 index 000000000..25f7c629e --- /dev/null +++ b/test/css-filter-test.php @@ -0,0 +1,157 @@ +#!/usr/bin/env php +_parseCss($css, $blocked); + } + }; + + $safeCss = $viewer->testParseCss($parser, false); + + echo "Input CSS:\n$maliciousCss\n\n"; + echo "Output (safe) CSS:\n$safeCss\n\n"; + + // Verify dangerous patterns are removed + $checks = [ + 'url(' => 'URLs should be removed', + '@import' => 'Imports should be removed', + 'cursor' => 'Cursor rules should be removed', + ]; + + $pass = 0; + $fail = 0; + + foreach ($checks as $pattern => $description) { + if (stripos($safeCss, $pattern) === false) { + echo "✓ PASS: $description\n"; + $pass++; + } else { + echo "✗ FAIL: $description (found: $pattern)\n"; + $fail++; + } + } + + // Verify safe CSS is kept + if (strpos($safeCss, 'color') !== false || strpos($safeCss, 'margin') !== false) { + echo "✓ PASS: Safe CSS properties preserved\n"; + $pass++; + } else { + echo "✗ FAIL: Safe CSS properties not preserved\n"; + $fail++; + } + + echo "\nTest 1 Results: $pass passed, $fail failed\n\n"; + +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . "\n"; + echo "Stack trace:\n" . $e->getTraceAsString() . "\n\n"; +} + +// Test 2: Blocked mode (keep only dangerous CSS) +echo "Test 2: Blocked Mode (Keep Only Dangerous CSS)\n"; +echo str_repeat("-", 60) . "\n"; + +try { + $parser2 = new Horde_Css_Parser($maliciousCss); + $blockedCss = $viewer->testParseCss($parser2, true); + + echo "Output (blocked) CSS:\n$blockedCss\n\n"; + + // Verify dangerous patterns are kept + $checks = [ + 'url(' => 'URLs should be kept', + '@import' => 'Imports should be kept', + 'cursor' => 'Cursor rules should be kept', + ]; + + $pass = 0; + $fail = 0; + + foreach ($checks as $pattern => $description) { + if (stripos($blockedCss, $pattern) !== false) { + echo "✓ PASS: $description\n"; + $pass++; + } else { + echo "✗ FAIL: $description (not found: $pattern)\n"; + $fail++; + } + } + + // Verify most safe CSS is removed + $safePatterns = ['color:', 'margin:']; + $safeRemoved = true; + foreach ($safePatterns as $pattern) { + if (stripos($blockedCss, $pattern) !== false) { + $safeRemoved = false; + break; + } + } + + if ($safeRemoved) { + echo "✓ PASS: Safe CSS properties removed\n"; + $pass++; + } else { + echo "✗ FAIL: Safe CSS properties not removed\n"; + $fail++; + } + + echo "\nTest 2 Results: $pass passed, $fail failed\n\n"; + +} catch (Exception $e) { + echo "ERROR: " . $e->getMessage() . "\n"; + echo "Stack trace:\n" . $e->getTraceAsString() . "\n\n"; +} + +echo str_repeat("=", 60) . "\n"; +echo "All tests complete!\n";