Repository: codeigniter4/CodeIgniter4 (PHP)

Issue:
Bug: view parser fails with `({variable})` in loop
### PHP Version

8.0

### CodeIgniter4 Version

4.1.8

### CodeIgniter4 Installation Method

Composer (using `codeigniter4/appstarter`)

### Which operating systems have you tested for this bug?

Windows

### Which server did you use?

apache

### Database

mysql

### What happened?

When using view parser to perform loop / variable substitution the final replacement
fails when a pair of parenthesis exist within an anchor tag's href attribute



### Steps to Reproduce

Controller Method:
```php
public function test()
{
    $somedata = [
        'message' => 'massagee',
        'userlist' => [
            ['id'=>10, 'email'=>'somemail@gmail.com']
        ]
    ];
    $parser = service('parser');
    echo $parser->setData($somedata)->render('test');
} 
```

Offending view code:
```html
{message}<br><br>
{userlist}
a row {id}, {email}, <a href="alert('{email}');">[unauthorize](https://ci4.knowledgematters.com/backend/alert('{email}');)</a><br>
{/userlist}
```


Output:
```html
massagee

{userlist} a row {id}, {email}, [unauthorize](https://ci4.knowledgematters.com/backend/somemail@gmail.com)
{/userlist}
```


Revised view code (alert() removed):
```html
{message}<br><br>
{userlist}
a row {id}, {email}, <a href="{email}">unauthorize</a><br>
{/userlist}
```


Properly parsed output:
```html
massagee

a row 10, somemail@gmail.com, unauthorize
```

### Expected Output

Expected proper parsing which happens when alert() is not present

### Anything else?

_No response_


Discussion:
```php
    public function index()
    {
        $parser = service('parser');

        $somedata = [
            'message' => 'massagee',
            'userlist' => [
                ['email'=>'somemail@gmail.com']
            ]
        ];
        $view = <<<'EOL'
            {message}<br><br>
            {userlist}
            {email}<br>
            {/userlist}
            EOL;

        return $parser->setData($somedata)->renderString($view);
    }
```
↓
```
massagee<br><br>

somemail@gmail.com<br>
```

But if I change the view:
```php
        $view = <<<'EOL'
            {message}<br><br>
            {userlist}
            ({email})<br>
            {/userlist}
            EOL;
```
↓
```
massagee<br><br>
{userlist}
({email})<br>
{/userlist}
```

@kenjis thank you for the example simplification

I dug deeper into the parser and determined this seems to be a failing with improperly escaped regex.

I am by no means a regex proficient; but the current fix I am using 
within Parser.php in the parsePair function is:

```php
// Escape | character from filters as it's handled as OR in regex
$escapedMatch = preg_replace('/(?<!\\\\)\\|/', '\\|', $match[0]);

$escapedMatch = addcslashes($escapedMatch, "()?{}"); //<-- fix here to escape parens, question mark, and braces

$replace['#' . $escapedMatch . '#s'] = $str;
```

I have no idea if this is a complete or even proper fix; but wanted to provide some additional information

@RaymondFJ Does the template work on CI3?
@kenjis 
> @RaymondFJ Does the template work on CI3?

Yes in our existing CI3 codebase we routinely would do things like javascript alerts with () of course
in looped table rows.

@RaymondFJ Thanks for reply.
I sent a PR to fix:  #5840
@kenjis 
Very cool, Thanks!

I look forward to giving it a try and not having to maintain my hack between CI updates.





Patch:
diff --git a/system/View/Parser.php b/system/View/Parser.php
index 6834c003e500..6f62ca4fb333 100644
--- a/system/View/Parser.php
+++ b/system/View/Parser.php
@@ -289,7 +289,7 @@ protected function parsePair(string $variable, array $data, string $template): a
          */
         foreach ($matches as $match) {
             // Loop over each piece of $data, replacing
-            // it's contents so that we know what to replace in parse()
+            // its contents so that we know what to replace in parse()
             $str = '';  // holds the new contents for this tag pair.
 
             foreach ($data as $row) {
@@ -338,8 +338,7 @@ protected function parsePair(string $variable, array $data, string $template): a
                 $str .= $out;
             }
 
-            // Escape | character from filters as it's handled as OR in regex
-            $escapedMatch = preg_replace('/(?<!\\\\)\\|/', '\\|', $match[0]);
+            $escapedMatch = preg_quote($match[0], '#');
 
             $replace['#' . $escapedMatch . '#s'] = $str;
         }
@@ -471,16 +470,8 @@ public function setDelimiters($leftDelimiter = '{', $rightDelimiter = '}'): Rend
      */
     protected function replaceSingle($pattern, $content, $template, bool $escape = false): string
     {
-        // Any dollar signs in the pattern will be misinterpreted, so slash them
-        $pattern = addcslashes($pattern, '$');
         $content = (string) $content;
 
-        // Flesh out the main pattern from the delimiters and escape the hash
-        // See https://regex101.com/r/IKdUlk/1
-        if (preg_match('/^(#)(.+)(#(m?s)?)$/s', $pattern, $parts)) {
-            $pattern = $parts[1] . addcslashes($parts[2], '#') . $parts[3];
-        }
-
         // Replace the content in the template
         return preg_replace_callback($pattern, function ($matches) use ($content, $escape) {
             // Check for {! !} syntax to not escape this one.


Test patch:
diff --git a/tests/system/View/ParserTest.php b/tests/system/View/ParserTest.php
index 3462b0cc2a84..2520c680ce0f 100644
--- a/tests/system/View/ParserTest.php
+++ b/tests/system/View/ParserTest.php
@@ -220,6 +220,26 @@ public function testParseLoop()
         $this->assertSame("Super Heroes\nTom Dick Henry ", $this->parser->renderString($template));
     }
 
+    /**
+     * @see https://github.com/codeigniter4/CodeIgniter4/issues/5825
+     */
+    public function testParseLoopVariableWithParentheses()
+    {
+        $data = [
+            'title'  => 'Super Heroes',
+            'powers' => [
+                ['name' => 'Tom'],
+                ['name' => 'Dick'],
+                ['name' => 'Henry'],
+            ],
+        ];
+
+        $template = "{title}\n{powers}({name}) {/powers}";
+
+        $this->parser->setData($data);
+        $this->assertSame("Super Heroes\n(Tom) (Dick) (Henry) ", $this->parser->renderString($template));
+    }
+
     public function testParseLoopObjectProperties()
     {
         $obj1 = new stdClass();