diff --git a/common.php b/common.php index ae654cf..b31c49b 100755 --- a/common.php +++ b/common.php @@ -10,7 +10,7 @@ use UniFi_API\Client as ApiClient; -const TOOL_VERSION = '2.0.30'; +const TOOL_VERSION = '2.0.31'; /** * Gather some basic information for the About modal. diff --git a/composer.lock b/composer.lock index f81afb7..5b64c1d 100755 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "art-of-wifi/unifi-api-client", - "version": "v1.1.99", + "version": "v1.1.100", "source": { "type": "git", "url": "https://github.com/Art-of-WiFi/UniFi-API-client.git", - "reference": "70f6a374e2c73eb91a9aa20f6c9375b235d55ce1" + "reference": "1522992e495f94b9fa52ff1015fe1e99f9a24fe4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Art-of-WiFi/UniFi-API-client/zipball/70f6a374e2c73eb91a9aa20f6c9375b235d55ce1", - "reference": "70f6a374e2c73eb91a9aa20f6c9375b235d55ce1", + "url": "https://api.github.com/repos/Art-of-WiFi/UniFi-API-client/zipball/1522992e495f94b9fa52ff1015fe1e99f9a24fe4", + "reference": "1522992e495f94b9fa52ff1015fe1e99f9a24fe4", "shasum": "" }, "require": { @@ -54,9 +54,9 @@ ], "support": { "issues": "https://github.com/Art-of-WiFi/UniFi-API-client/issues", - "source": "https://github.com/Art-of-WiFi/UniFi-API-client/tree/v1.1.99" + "source": "https://github.com/Art-of-WiFi/UniFi-API-client/tree/v1.1.100" }, - "time": "2024-10-23T11:30:34+00:00" + "time": "2024-10-29T11:14:00+00:00" }, { "name": "kint-php/kint", @@ -516,16 +516,16 @@ }, { "name": "twig/twig", - "version": "v3.11.1", + "version": "v3.11.3", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "ff063afc691e1cfda6714f1915ed766cb108d188" + "reference": "3b06600ff3abefaf8ff55d5c336cd1c4253f8c7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/ff063afc691e1cfda6714f1915ed766cb108d188", - "reference": "ff063afc691e1cfda6714f1915ed766cb108d188", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3b06600ff3abefaf8ff55d5c336cd1c4253f8c7e", + "reference": "3b06600ff3abefaf8ff55d5c336cd1c4253f8c7e", "shasum": "" }, "require": { @@ -580,7 +580,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.11.1" + "source": "https://github.com/twigphp/Twig/tree/v3.11.3" }, "funding": [ { @@ -592,7 +592,7 @@ "type": "tidelift" } ], - "time": "2024-09-10T10:40:14+00:00" + "time": "2024-11-07T12:34:41+00:00" } ], "packages-dev": [], diff --git a/js/custom.js b/js/custom.js index 1674db8..78f9fe2 100755 --- a/js/custom.js +++ b/js/custom.js @@ -194,7 +194,7 @@ $('.output_radio_button').click(function() { */ function switchCSS(new_theme) { console.log('switching to new Bootswatch theme: ' + new_theme); - if (new_theme == 'bootstrap') { + if (new_theme === 'bootstrap') { $('#bootswatch_theme_stylesheet').attr('href', ''); } else { $('#bootswatch_theme_stylesheet').attr('href', 'https://stackpath.bootstrapcdn.com/bootswatch/4.3.1/' + new_theme + '/bootstrap.min.css'); @@ -317,7 +317,7 @@ function fetchDebugDetails() { url: 'ajax/show_api_debug.php', dataType: 'html', success: function (data) { - if (data != 'ignore') { + if (data !== 'ignore') { console.log('debug messages as returned by the cURL request to the UniFi controller API:'); console.log(data); } @@ -532,28 +532,37 @@ $(function() { * upon opening the "About" modal we check latest version of API browser tool using AJAX and inform user when it's * more recent than the current */ +let version_update_span = $('#span_api_browser_update'); $('#about_modal').on('shown.bs.modal', function (e) { $.ajax({ type: 'GET', url: 'https://api.github.com/repos/Art-of-WiFi/UniFi-API-browser/releases/latest', dataType: 'json', success: function (json) { - if (api_browser_version != '' && typeof(json.tag_name) !== 'undefined') { - if (cmpVersion(api_browser_version, json.tag_name.substring(1)) < 0) { - $('#span_api_browser_update').html('an update is available: ' + json.tag_name.substring(1)); + if (api_browser_version !== '' && typeof(json.tag_name) !== 'undefined') { + const normalizedTagName = json.tag_name.startsWith('v') ? json.tag_name.substring(1) : json.tag_name; + + if (debug) { + console.log('API Browser Version:', api_browser_version); + console.log('Normalized Tag Name:', normalizedTagName); + console.log('Comparison Result:', cmpVersion(api_browser_version, normalizedTagName)); + } + + if (cmpVersion(api_browser_version, normalizedTagName) < 0) { + version_update_span.html('an update is available: ' + normalizedTagName); $('#span_api_browser_update').removeClass('badge-success').addClass('badge-warning'); - } else if (cmpVersion(api_browser_version, json.tag_name.substring(1)) === 0) { - $('#span_api_browser_update').html('up to date'); - $('#span_api_browser_update').removeClass('badge-danger').addClass('badge-success'); + } else if (cmpVersion(api_browser_version, normalizedTagName) === 0) { + version_update_span.html('up to date'); + version_update_span.removeClass('badge-danger').addClass('badge-success'); } else { - $('#span_api_browser_update').html('bleeding edge!'); - $('#span_api_browser_update').removeClass('badge-success').addClass('badge-danger'); + version_update_span.html('bleeding edge!'); + version_update_span.removeClass('badge-success').addClass('badge-danger'); } } }, error: function(jqXHR, textStatus, errorThrown) { - $('#span_api_browser_update').html('error checking updates'); - $('#span_api_browser_update').removeClass('badge-success').addClass('badge-danger'); + version_update_span.html('error checking updates'); + version_update_span.removeClass('badge-success').addClass('badge-danger'); console.log(jqXHR); } }); diff --git a/vendor/art-of-wifi/unifi-api-client/src/Client.php b/vendor/art-of-wifi/unifi-api-client/src/Client.php index ad3f76e..2b4fd91 100755 --- a/vendor/art-of-wifi/unifi-api-client/src/Client.php +++ b/vendor/art-of-wifi/unifi-api-client/src/Client.php @@ -20,7 +20,7 @@ class Client { /** Constants. */ - const CLASS_VERSION = '1.1.99'; + const CLASS_VERSION = '1.1.100'; const CURL_METHODS_ALLOWED = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']; const DEFAULT_CURL_METHOD = 'GET'; @@ -2098,7 +2098,8 @@ public function list_hotspotop() * @param int|null $up upload speed limit in kbps * @param int|null $down download speed limit in kbps * @param int|null $megabytes data transfer limit in MB - * @return array containing a single object which contains the create_time(stamp) of the voucher(s) created + * @return array|bool containing a single object/array which contains the create_time(stamp) of the voucher(s) + * created, false upon failure */ public function create_voucher( int $minutes, @@ -2108,7 +2109,7 @@ public function create_voucher( int $up = null, int $down = null, int $megabytes = null - ): array + ) { $payload = [ 'cmd' => 'create-voucher', diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 75bcb3e..64891d7 100755 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -2,17 +2,17 @@ "packages": [ { "name": "art-of-wifi/unifi-api-client", - "version": "v1.1.99", - "version_normalized": "1.1.99.0", + "version": "v1.1.100", + "version_normalized": "1.1.100.0", "source": { "type": "git", "url": "https://github.com/Art-of-WiFi/UniFi-API-client.git", - "reference": "70f6a374e2c73eb91a9aa20f6c9375b235d55ce1" + "reference": "1522992e495f94b9fa52ff1015fe1e99f9a24fe4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Art-of-WiFi/UniFi-API-client/zipball/70f6a374e2c73eb91a9aa20f6c9375b235d55ce1", - "reference": "70f6a374e2c73eb91a9aa20f6c9375b235d55ce1", + "url": "https://api.github.com/repos/Art-of-WiFi/UniFi-API-client/zipball/1522992e495f94b9fa52ff1015fe1e99f9a24fe4", + "reference": "1522992e495f94b9fa52ff1015fe1e99f9a24fe4", "shasum": "" }, "require": { @@ -20,7 +20,7 @@ "ext-json": "*", "php": ">=7.4.0" }, - "time": "2024-10-23T11:30:34+00:00", + "time": "2024-10-29T11:14:00+00:00", "type": "library", "installation-source": "source", "autoload": { @@ -51,7 +51,7 @@ ], "support": { "issues": "https://github.com/Art-of-WiFi/UniFi-API-client/issues", - "source": "https://github.com/Art-of-WiFi/UniFi-API-client/tree/v1.1.99" + "source": "https://github.com/Art-of-WiFi/UniFi-API-client/tree/v1.1.100" }, "install-path": "../art-of-wifi/unifi-api-client" }, @@ -531,17 +531,17 @@ }, { "name": "twig/twig", - "version": "v3.11.1", - "version_normalized": "3.11.1.0", + "version": "v3.11.3", + "version_normalized": "3.11.3.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "ff063afc691e1cfda6714f1915ed766cb108d188" + "reference": "3b06600ff3abefaf8ff55d5c336cd1c4253f8c7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/ff063afc691e1cfda6714f1915ed766cb108d188", - "reference": "ff063afc691e1cfda6714f1915ed766cb108d188", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/3b06600ff3abefaf8ff55d5c336cd1c4253f8c7e", + "reference": "3b06600ff3abefaf8ff55d5c336cd1c4253f8c7e", "shasum": "" }, "require": { @@ -556,7 +556,7 @@ "psr/container": "^1.0|^2.0", "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, - "time": "2024-09-10T10:40:14+00:00", + "time": "2024-11-07T12:34:41+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -598,7 +598,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.11.1" + "source": "https://github.com/twigphp/Twig/tree/v3.11.3" }, "funding": [ { diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 5d96265..be73c41 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '2aa4eacd2a3939191239784be9a1f9a622eb5db1', + 'reference' => '1d0784845357ba781472c9ea6b6dd07ba870cae3', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -13,16 +13,16 @@ '__root__' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '2aa4eacd2a3939191239784be9a1f9a622eb5db1', + 'reference' => '1d0784845357ba781472c9ea6b6dd07ba870cae3', 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => false, ), 'art-of-wifi/unifi-api-client' => array( - 'pretty_version' => 'v1.1.99', - 'version' => '1.1.99.0', - 'reference' => '70f6a374e2c73eb91a9aa20f6c9375b235d55ce1', + 'pretty_version' => 'v1.1.100', + 'version' => '1.1.100.0', + 'reference' => '1522992e495f94b9fa52ff1015fe1e99f9a24fe4', 'type' => 'library', 'install_path' => __DIR__ . '/../art-of-wifi/unifi-api-client', 'aliases' => array(), @@ -83,9 +83,9 @@ 'dev_requirement' => false, ), 'twig/twig' => array( - 'pretty_version' => 'v3.11.1', - 'version' => '3.11.1.0', - 'reference' => 'ff063afc691e1cfda6714f1915ed766cb108d188', + 'pretty_version' => 'v3.11.3', + 'version' => '3.11.3.0', + 'reference' => '3b06600ff3abefaf8ff55d5c336cd1c4253f8c7e', 'type' => 'library', 'install_path' => __DIR__ . '/../twig/twig', 'aliases' => array(), diff --git a/vendor/twig/twig/CHANGELOG b/vendor/twig/twig/CHANGELOG index 55285d6..525e238 100644 --- a/vendor/twig/twig/CHANGELOG +++ b/vendor/twig/twig/CHANGELOG @@ -1,3 +1,14 @@ +# 3.11.3 (2024-11-07) + + * Fix an infinite recursion in the sandbox code + +# 3.11.2 (2024-11-06) + + * [BC BREAK] Fix a security issue in the sandbox mode allowing an attacker to call attributes on Array-like objects + They are now checked via the property policy + * Fix a security issue in the sandbox mode allowing an attacker to be able to call `toString()` + under some circumstances on an object even if the `__toString()` method is not allowed by the security policy + # 3.11.1 (2024-09-10) * Fix a security issue when an included sandboxed template has been loaded before without the sandbox context diff --git a/vendor/twig/twig/src/Environment.php b/vendor/twig/twig/src/Environment.php index e928e63..571b461 100644 --- a/vendor/twig/twig/src/Environment.php +++ b/vendor/twig/twig/src/Environment.php @@ -43,11 +43,11 @@ */ class Environment { - public const VERSION = '3.11.1'; - public const VERSION_ID = 301101; + public const VERSION = '3.11.3'; + public const VERSION_ID = 301103; public const MAJOR_VERSION = 4; public const MINOR_VERSION = 11; - public const RELEASE_VERSION = 1; + public const RELEASE_VERSION = 3; public const EXTRA_VERSION = ''; private $charset; diff --git a/vendor/twig/twig/src/Extension/CoreExtension.php b/vendor/twig/twig/src/Extension/CoreExtension.php index 4b014b8..b077e79 100644 --- a/vendor/twig/twig/src/Extension/CoreExtension.php +++ b/vendor/twig/twig/src/Extension/CoreExtension.php @@ -57,6 +57,8 @@ use Twig\Node\Expression\Unary\NotUnary; use Twig\Node\Expression\Unary\PosUnary; use Twig\NodeVisitor\MacroAutoImportNodeVisitor; +use Twig\Sandbox\SecurityNotAllowedMethodError; +use Twig\Sandbox\SecurityNotAllowedPropertyError; use Twig\Source; use Twig\Template; use Twig\TemplateWrapper; @@ -82,6 +84,20 @@ final class CoreExtension extends AbstractExtension { + public const ARRAY_LIKE_CLASSES = [ + 'ArrayIterator', + 'ArrayObject', + 'CachingIterator', + 'RecursiveArrayIterator', + 'RecursiveCachingIterator', + 'SplDoublyLinkedList', + 'SplFixedArray', + 'SplObjectStorage', + 'SplQueue', + 'SplStack', + 'WeakMap', + ]; + private $dateFormats = ['F j, Y H:i', '%d days']; private $numberFormat = [0, '.', ',']; private $timezone = null; @@ -1549,10 +1565,20 @@ public static function batch($items, $size, $fill = null, $preserveKeys = true): */ public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = /* Template::ANY_CALL */ 'any', $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) { + $propertyNotAllowedError = null; + // array if (/* Template::METHOD_CALL */ 'method' !== $type) { $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + if ($sandboxed && $object instanceof \ArrayAccess && !\in_array(get_class($object), self::ARRAY_LIKE_CLASSES, true)) { + try { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $arrayItem, $lineno, $source); + } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) { + goto methodCheck; + } + } + if (((\is_array($object) || $object instanceof \ArrayObject) && (isset($object[$arrayItem]) || \array_key_exists($arrayItem, (array) $object))) || ($object instanceof \ArrayAccess && isset($object[$arrayItem])) ) { @@ -1624,19 +1650,25 @@ public static function getAttribute(Environment $env, Source $source, $object, $ // object property if (/* Template::METHOD_CALL */ 'method' !== $type) { + if ($sandboxed) { + try { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) { + goto methodCheck; + } + } + if (isset($object->$item) || \array_key_exists((string) $item, (array) $object)) { if ($isDefinedTest) { return true; } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); - } - return $object->$item; } } + methodCheck: + static $cache = []; $class = \get_class($object); @@ -1695,6 +1727,10 @@ public static function getAttribute(Environment $env, Source $source, $object, $ return false; } + if ($propertyNotAllowedError) { + throw $propertyNotAllowedError; + } + if ($ignoreStrictCheck || !$env->isStrictVariables()) { return; } @@ -1702,12 +1738,24 @@ public static function getAttribute(Environment $env, Source $source, $object, $ throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()"/"is%1$s()"/"has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); } - if ($isDefinedTest) { - return true; + if ($sandboxed) { + try { + $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + } catch (SecurityNotAllowedMethodError $e) { + if ($isDefinedTest) { + return false; + } + + if ($propertyNotAllowedError) { + throw $propertyNotAllowedError; + } + + throw $e; + } } - if ($sandboxed) { - $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + if ($isDefinedTest) { + return true; } // Some objects throw exceptions when they have __call, and the method we try diff --git a/vendor/twig/twig/src/Extension/SandboxExtension.php b/vendor/twig/twig/src/Extension/SandboxExtension.php index 921df28..255f2f2 100644 --- a/vendor/twig/twig/src/Extension/SandboxExtension.php +++ b/vendor/twig/twig/src/Extension/SandboxExtension.php @@ -119,6 +119,12 @@ public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null) { + if (\is_array($obj)) { + $this->ensureToStringAllowedForArray($obj, $lineno, $source); + + return $obj; + } + if ($this->isSandboxed($source) && \is_object($obj) && method_exists($obj, '__toString')) { try { $this->policy->checkMethodAllowed($obj, '__toString'); @@ -132,4 +138,45 @@ public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = return $obj; } + + private function ensureToStringAllowedForArray(array $obj, int $lineno, ?Source $source, array &$stack = []): void + { + foreach ($obj as $k => $v) { + if (!$v) { + continue; + } + + if (!\is_array($v)) { + $this->ensureToStringAllowed($v, $lineno, $source); + continue; + } + + if (\PHP_VERSION_ID < 70400) { + static $cookie; + + if ($v === $cookie ?? $cookie = new \stdClass()) { + continue; + } + + $obj[$k] = $cookie; + try { + $this->ensureToStringAllowedForArray($v, $lineno, $source, $stack); + } finally { + $obj[$k] = $v; + } + + continue; + } + + if ($r = \ReflectionReference::fromArrayElement($obj, $k)) { + if (isset($stack[$r->getId()])) { + continue; + } + + $stack[$r->getId()] = true; + } + + $this->ensureToStringAllowedForArray($v, $lineno, $source, $stack); + } + } } diff --git a/vendor/twig/twig/src/Node/Expression/GetAttrExpression.php b/vendor/twig/twig/src/Node/Expression/GetAttrExpression.php index 29a446b..f54f2f0 100644 --- a/vendor/twig/twig/src/Node/Expression/GetAttrExpression.php +++ b/vendor/twig/twig/src/Node/Expression/GetAttrExpression.php @@ -31,6 +31,7 @@ public function __construct(AbstractExpression $node, AbstractExpression $attrib public function compile(Compiler $compiler): void { $env = $compiler->getEnvironment(); + $arrayAccessSandbox = false; // optimize array calls if ( @@ -44,17 +45,35 @@ public function compile(Compiler $compiler): void ->raw('(('.$var.' = ') ->subcompile($this->getNode('node')) ->raw(') && is_array(') - ->raw($var) + ->raw($var); + + if (!$env->hasExtension(SandboxExtension::class)) { + $compiler + ->raw(') || ') + ->raw($var) + ->raw(' instanceof ArrayAccess ? (') + ->raw($var) + ->raw('[') + ->subcompile($this->getNode('attribute')) + ->raw('] ?? null) : null)') + ; + + return; + } + + $arrayAccessSandbox = true; + + $compiler ->raw(') || ') ->raw($var) - ->raw(' instanceof ArrayAccess ? (') + ->raw(' instanceof ArrayAccess && in_array(') + ->raw('get_class('.$var.')') + ->raw(', CoreExtension::ARRAY_LIKE_CLASSES, true) ? (') ->raw($var) ->raw('[') ->subcompile($this->getNode('attribute')) - ->raw('] ?? null) : null)') + ->raw('] ?? null) : ') ; - - return; } $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); @@ -83,5 +102,9 @@ public function compile(Compiler $compiler): void ->raw(', ')->repr($this->getNode('node')->getTemplateLine()) ->raw(')') ; + + if ($arrayAccessSandbox) { + $compiler->raw(')'); + } } } diff --git a/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php b/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php index 6802088..7cc1c2e 100644 --- a/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php +++ b/vendor/twig/twig/src/NodeVisitor/SandboxNodeVisitor.php @@ -15,12 +15,14 @@ use Twig\Node\CheckSecurityCallNode; use Twig\Node\CheckSecurityNode; use Twig\Node\CheckToStringNode; +use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\Binary\ConcatBinary; use Twig\Node\Expression\Binary\RangeBinary; use Twig\Node\Expression\FilterExpression; use Twig\Node\Expression\FunctionExpression; use Twig\Node\Expression\GetAttrExpression; use Twig\Node\Expression\NameExpression; +use Twig\Node\Expression\Unary\SpreadUnary; use Twig\Node\ModuleNode; use Twig\Node\Node; use Twig\Node\PrintNode; @@ -120,7 +122,18 @@ private function wrapNode(Node $node, string $name): void { $expr = $node->getNode($name); if (($expr instanceof NameExpression || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) { - $node->setNode($name, new CheckToStringNode($expr)); + // Simplify in 4.0 as the spread attribute has been removed there + $new = new CheckToStringNode($expr); + if ($expr->hasAttribute('spread')) { + $new->setAttribute('spread', $expr->getAttribute('spread')); + } + $node->setNode($name, $new); + } elseif ($expr instanceof SpreadUnary) { + $this->wrapNode($expr, 'node'); + } elseif ($expr instanceof ArrayExpression) { + foreach ($expr as $name => $_) { + $this->wrapNode($expr, $name); + } } }