[TikiWiki-commits] [Git][tikiwiki/tiki][29.x] [FIX] Manticore: handling wildcards in regex

"Victor Emanouilov \(@kroky\) via TikiWiki-cvs" <[email protected]>
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <69f30a7d31091_381c7ef104480@gitlab-sidekiq-low-urgency-cpu-bound-v2-5599fc94bd-rbmbf.mail>

Victor Emanouilov pushed to branch 29.x at Tiki Wiki CMS Groupware / Tiki


Commits:
98859c1e by Victor Emanouilov at 2026-04-30T10:53:23+03:00
[FIX] Manticore: handling wildcards in regex
---
* [FIX] Manticore: handling wildcards in regex resulted in parse error or incorrect regex syntax when fulltext was scaled down to regex due to missing fulltext field index

See merge request tikiwiki/tiki!10112

- - - - -


2 changed files:

- lib/core/Search/Manticore/QueryBuilder.php
- + lib/test/core/Search/Manticore/WildcardTest.php


Changes:

=====================================
lib/core/Search/Manticore/QueryBuilder.php
=====================================
@@ -226,7 +226,7 @@ class QueryBuilder
         } elseif ($node instanceof Initial) {
             $field = $this->getField($node);
             Index::addSearchedField($node->getField(), 'others');
-            $value = $this->getQuoted($node, '(?i)^');
+            $value = $this->quoteRegex($node, '(?i)^');
             $key = 'tf_' . uniqid();
             $this->select[$key] = "REGEX(TO_STRING({$field}), $value)";
             return "$key = 1";
@@ -295,7 +295,7 @@ class QueryBuilder
                 $this->select[$key] = "TO_STRING({$field}) = $value";
                 return "$key = 1";
             } else {
-                $value = $this->getQuoted($node, '(?i)');
+                $value = $this->quoteRegex($node, '(?i)');
                 $key = 'tf_' . uniqid();
                 $this->select[$key] = "REGEX(TO_STRING({$field}), $value)";
                 return "$key = 1";
@@ -310,17 +310,18 @@ class QueryBuilder
             $value = $this->getQuoted($node);
             return "{$field} = $value";
         } else {
-            $value = $this->getQuoted($node, '(?i)');
-            if (is_array($value)) {
+            $rawValue = $this->getRaw($node);
+            if (is_array($rawValue)) {
                 return '(' . implode(' OR ', array_filter(array_map(function ($v) use ($field) {
                     if (is_scalar($v)) {
-                        $v = $this->pdo_client->quote('(?i)' . strval($v));
+                        $v = $this->pdo_client->quote('(?i)' . $this->wildcardToRe2(strval($v)));
                     } else {
                         return null;
                     }
                     return "REGEX({$field}, $v)";
-                }, $value))) . ')';
+                }, $rawValue))) . ')';
             } else {
+                $value = $this->quoteRegex($node, '(?i)');
                 return "REGEX({$field}, $value)";
             }
         }
@@ -393,6 +394,20 @@ class QueryBuilder
         }
     }
 
+    private function quoteRegex($node, $prefix)
+    {
+        return $this->pdo_client->quote($prefix . $this->wildcardToRe2(strval($this->getRaw($node))));
+    }
+
+    private function wildcardToRe2($value)
+    {
+        if ($value === '') {
+            return $value;
+        }
+        $escaped = preg_quote($value, '/');
+        return str_replace('\\*', '.*', $escaped);
+    }
+
     private function getRaw($node, $forceType = null)
     {
         $value = $node->getValue($this->factory);


=====================================
lib/test/core/Search/Manticore/WildcardTest.php
=====================================
@@ -0,0 +1,172 @@
+<?php
+
+// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
+//
+// All Rights Reserved. See copyright.txt for details and a complete list of authors.
+// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
+namespace Search\Manticore;
+
+use ReflectionMethod;
+use Search_Expr_Token;
+use Search_Query;
+
+/**
+ * Verifies how the Manticore QueryBuilder turns wildcard ("*foo*") and
+ * regex-special user input into RE2 patterns inside REGEX() calls.
+ *
+ * The object selector (and similar callers) wrap user input with "*" to
+ * emulate substring search. Because Manticore evaluates these patterns
+ * with RE2, a leading "*" is invalid and previously caused a
+ * "no argument for repetition operator: *" parse error.
+ */
+class WildcardTest extends \PHPUnit\Framework\TestCase
+{
+    use IndexBuilder;
+
+    public $index;
+    private $old_prefs;
+
+    protected function setUp(): void
+    {
+        global $prefs;
+        $this->old_prefs = $prefs;
+
+        $this->index = $this->createIndex('_wildcard');
+        $this->index->destroy();
+
+        $typeFactory = $this->index->getTypeFactory();
+        $this->index->addDocument(
+            [
+                'object_type' => $typeFactory->identifier('trackeritem'),
+                'object_id' => $typeFactory->identifier('1'),
+                'title' => $typeFactory->plaintext('Michaela Smith'),
+                'tracker_field_name' => $typeFactory->sortable('Michaela Smith'),
+            ]
+        );
+        $this->index->addDocument(
+            [
+                'object_type' => $typeFactory->identifier('trackeritem'),
+                'object_id' => $typeFactory->identifier('2'),
+                'title' => $typeFactory->plaintext('John Doe'),
+                'tracker_field_name' => $typeFactory->sortable('John Doe'),
+            ]
+        );
+        $this->index->addDocument(
+            [
+                'object_type' => $typeFactory->identifier('trackeritem'),
+                'object_id' => $typeFactory->identifier('3'),
+                'title' => $typeFactory->plaintext('first.middle last'),
+                'tracker_field_name' => $typeFactory->sortable('first.middle last'),
+            ]
+        );
+        $this->index->addDocument(
+            [
+                'object_type' => $typeFactory->identifier('trackeritem'),
+                'object_id' => $typeFactory->identifier('4'),
+                'title' => $typeFactory->plaintext('firstXmiddle last'),
+                'tracker_field_name' => $typeFactory->sortable('firstXmiddle last'),
+            ]
+        );
+    }
+
+    protected function tearDown(): void
+    {
+        global $prefs;
+        $prefs = $this->old_prefs;
+
+        if ($this->index) {
+            $this->index->destroy();
+        }
+    }
+
+    public function testLeadingAndTrailingWildcardsAreAccepted()
+    {
+        $query = $this->newQuery();
+        $query->filterContent('*michaela*', 'tracker_field_name');
+        $this->assertCount(1, $query->search($this->index));
+    }
+
+    public function testWildcardOnlyMatchesAnything()
+    {
+        $query = $this->newQuery();
+        $query->filterContent('*', 'tracker_field_name');
+        $this->assertCount(4, $query->search($this->index));
+    }
+
+    public function testInnerWildcardActsAsGlob()
+    {
+        $query = $this->newQuery();
+        $query->filterContent('first*last', 'tracker_field_name');
+        // Should match both "first.middle last" and "firstXmiddle last".
+        $this->assertCount(2, $query->search($this->index));
+    }
+
+    public function testRegexMetacharactersAreTreatedLiterally()
+    {
+        $query = $this->newQuery();
+        $query->filterContent('first.middle', 'tracker_field_name');
+        $this->assertCount(1, $query->search($this->index));
+    }
+
+    public function testWildcardToRe2EscapesMetacharacters()
+    {
+        $convert = new ReflectionMethod(QueryBuilder::class, 'wildcardToRe2');
+        $convert->setAccessible(true);
+
+        $builder = new QueryBuilder($this->index);
+
+        $cases = [
+            // input              => expected RE2 body
+            ''                    => '',
+            'michaela'            => 'michaela',
+            '*michaela*'          => '.*michaela.*',
+            'foo*bar'             => 'foo.*bar',
+            '*'                   => '.*',
+            'foo.bar'             => 'foo\\.bar',
+            'a+b'                 => 'a\\+b',
+            'a(b)c'               => 'a\\(b\\)c',
+        ];
+
+        foreach ($cases as $input => $expected) {
+            $this->assertSame(
+                $expected,
+                $convert->invoke($builder, $input),
+                'wildcardToRe2 for input ' . var_export($input, true)
+            );
+        }
+    }
+
+    public function testQuoteRegexBuildsExpectedPattern()
+    {
+        $quote = new ReflectionMethod(QueryBuilder::class, 'quoteRegex');
+        $quote->setAccessible(true);
+
+        $builder = new QueryBuilder($this->index);
+
+        $token = new Search_Expr_Token('*michaela*');
+        $token->setType('plaintext');
+        $token->setField('tracker_field_name');
+
+        $this->assertSame(
+            "'(?i).*michaela.*'",
+            $quote->invoke($builder, $token, '(?i)')
+        );
+
+        $token = new Search_Expr_Token('mich');
+        $token->setType('plaintext');
+        $token->setField('tracker_field_name');
+
+        $this->assertSame(
+            "'(?i)^mich'",
+            $quote->invoke($builder, $token, '(?i)^')
+        );
+    }
+
+    private function newQuery(): Search_Query
+    {
+        $query = new Search_Query();
+        \TikiLib::lib('unifiedsearch')->initQuery($query);
+        $query->filterType('trackeritem');
+        return $query;
+    }
+}



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/98859c1e00edcc6d81ae44ef4699fb0318666acb

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/98859c1e00edcc6d81ae44ef4699fb0318666acb
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.