diff --git a/lib/VM.php b/lib/VM.php index 54c9e7d55..a342b9af6 100755 --- a/lib/VM.php +++ b/lib/VM.php @@ -56,19 +56,19 @@ public function run(Block $block): int { break; case OpCode::TYPE_ARRAY_DIM_FETCH: $arg1 = $frame->scope[$op->arg1]; - $arg2 = $frame->scope[$op->arg2]; + $container = $frame->scope[$op->arg2]->resolveIndirect(); if (is_null($op->arg3)) { - if ($arg2->type !== Variable::TYPE_ARRAY) { + if ($container->type !== Variable::TYPE_ARRAY) { throw new \LogicException('[] is only supported for arrays'); } - $arg1->indirect($arg2->toArray()->append(new Variable)); + $arg1->indirect($container->toArray()->append(new Variable)); break; } $arg3 = $frame->scope[$op->arg3]; - if ($arg2->type === Variable::TYPE_STRING) { - $arg1->string($arg2->toString()[$arg3->toInt()]); - } elseif ($arg2->type === Variable::TYPE_ARRAY) { - $arg1->indirect($arg2->toArray()->findVariable($arg3, false)); + if ($container->type === Variable::TYPE_STRING) { + $arg1->string($container->toString()[$arg3->toInt()]); + } elseif ($container->type === Variable::TYPE_ARRAY) { + $arg1->indirect($container->toArray()->findVariable($arg3, false)); } else { throw new \LogicException('Illegal offset'); } diff --git a/lib/Web/Superglobals.php b/lib/Web/Superglobals.php index 4c1ae939f..670b0aa1a 100644 --- a/lib/Web/Superglobals.php +++ b/lib/Web/Superglobals.php @@ -163,11 +163,88 @@ private static function populateFormEncoded(HashTable $ht, string $body): void } $params = []; parse_str($body, $params); + self::mergeParsedParams($ht, $params); + } + + /** + * Merge PHP parse_str() output into a VM hashtable (supports nested keys and lists). + * + * @param array $params + */ + private static function mergeParsedParams(HashTable $ht, array $params): void + { foreach ($params as $key => $value) { - if (!is_string($key) || is_array($value)) { + if (is_array($value)) { + $child = self::ensureArrayChild($ht, $key); + self::mergeParsedParams($child, $value); + + continue; + } + if (!is_scalar($value)) { continue; } - self::setStringEntry($ht, $key, (string) $value); + self::setScalarEntry($ht, $key, $value); + } + } + + /** + * @param int|string $key + */ + private static function ensureArrayChild(HashTable $ht, $key): HashTable + { + $existing = is_int($key) ? $ht->findIndex($key) : $ht->find((string) $key); + if (null !== $existing) { + $resolved = $existing->resolveIndirect(); + if (Variable::TYPE_ARRAY === $resolved->type) { + return $resolved->toArray(); + } + } + + $nested = new HashTable(); + $var = new Variable(Variable::TYPE_ARRAY); + $var->array($nested); + if (null !== $existing) { + $existing->copyFrom($var); + } elseif (is_int($key)) { + $ht->addIndex($key, $var); + } else { + $ht->add((string) $key, $var); + } + + return $nested; + } + + /** + * @param int|string $key + * @param bool|float|int|string $value + */ + private static function setScalarEntry(HashTable $ht, $key, $value): void + { + $var = new Variable(); + if (is_int($value)) { + $var->int($value); + } elseif (is_float($value)) { + $var->float($value); + } elseif (is_bool($value)) { + $var->bool($value); + } else { + $var->string((string) $value); + } + if (is_int($key)) { + $existing = $ht->findIndex($key); + if (null !== $existing) { + $existing->copyFrom($var); + } else { + $ht->addIndex($key, $var); + } + + return; + } + $existing = $ht->find((string) $key); + if (null !== $existing) { + $existing->copyFrom($var); + } else { + $ht->add((string) $key, $var); } } diff --git a/test/real/cases/web_get_array_params.phpt b/test/real/cases/web_get_array_params.phpt new file mode 100644 index 000000000..41374c094 --- /dev/null +++ b/test/real/cases/web_get_array_params.phpt @@ -0,0 +1,11 @@ +--TEST-- +Web: array query parameters (tags[]=a&user[name]=Ada) +--ENV-- +QUERY_STRING=tags[]=a&tags[]=b&user[name]=Ada +--FILE-- +vmContext, + 'tags[]=a&tags[]=b&user[name]=Ada', + '' + ); + + $get = $runtime->vmContext->getSuperglobal('_GET'); + $this->assertNotNull($get); + $table = $get->toArray(); + + $tagsKey = new VMVariable(); + $tagsKey->string('tags'); + $this->assertTrue($table->offsetIsSet($tagsKey)); + $tags = $table->find('tags')->resolveIndirect()->toArray(); + + $zero = new VMVariable(); + $zero->int(0); + $one = new VMVariable(); + $one->int(1); + $this->assertTrue($tags->offsetIsSet($zero)); + $this->assertTrue($tags->offsetIsSet($one)); + $this->assertSame('a', $tags->findIndex(0)->resolveIndirect()->toString()); + $this->assertSame('b', $tags->findIndex(1)->resolveIndirect()->toString()); + + $userKey = new VMVariable(); + $userKey->string('user'); + $this->assertTrue($table->offsetIsSet($userKey)); + $user = $table->find('user')->resolveIndirect()->toArray(); + + $nameKey = new VMVariable(); + $nameKey->string('name'); + $this->assertTrue($user->offsetIsSet($nameKey)); + $this->assertSame('Ada', $user->find('name')->resolveIndirect()->toString()); + } + + public function testNestedPostBody(): void + { + $runtime = new Runtime(); + Superglobals::populateFromEnvironment( + $runtime->vmContext, + '', + 'items[]=x&items[]=y&meta[ok]=1' + ); + + $post = $runtime->vmContext->getSuperglobal('_POST'); + $this->assertNotNull($post); + $table = $post->toArray(); + + $items = $table->find('items')->resolveIndirect()->toArray(); + $this->assertSame('x', $items->findIndex(0)->resolveIndirect()->toString()); + $this->assertSame('y', $items->findIndex(1)->resolveIndirect()->toString()); + + $meta = $table->find('meta')->resolveIndirect()->toArray(); + $this->assertSame('1', $meta->find('ok')->resolveIndirect()->toString()); + } +}