[!!!][BUGFIX] Indexed Search: Escape search strings for LIKE queries 69/42969/13
authorTymoteusz Motylewski <t.motylewski@gmail.com>
Tue, 1 Sep 2015 19:22:31 +0000 (21:22 +0200)
committerBenni Mack <benni@typo3.org>
Tue, 29 Sep 2015 12:53:38 +0000 (14:53 +0200)
LIKE queries support special placeholders (_ and %).
These characters need proper escaping before being used
in database queries. Use the escapeStrForLike() method to
provide properly escaped strings to the query.

This patch handles few places missed by http://review.typo3.org/42806

Resolves: #69227
Releases: master
Change-Id: Ie68dd140aafa755be08ba9505b3ac33f00a8f2e2
Reviewed-on: http://review.typo3.org/42969
Reviewed-by: Alexander Opitz <opitz.alexander@googlemail.com>
Tested-by: Alexander Opitz <opitz.alexander@googlemail.com>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Benni Mack <benni@typo3.org>
typo3/sysext/core/Documentation/Changelog/master/Breaking-69227-StringsForLikeAreNotProperlyEscaped.rst [new file with mode: 0644]
typo3/sysext/indexed_search/Classes/Controller/SearchFormController.php
typo3/sysext/indexed_search/Classes/Domain/Repository/IndexSearchRepository.php
typo3/sysext/indexed_search/Classes/Utility/LikeWildcard.php [new file with mode: 0644]
typo3/sysext/indexed_search/Tests/Unit/Utility/LikeWildcardTest.php [new file with mode: 0644]

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Breaking-69227-StringsForLikeAreNotProperlyEscaped.rst b/typo3/sysext/core/Documentation/Changelog/master/Breaking-69227-StringsForLikeAreNotProperlyEscaped.rst
new file mode 100644 (file)
index 0000000..353e5eb
--- /dev/null
@@ -0,0 +1,34 @@
+============================================================
+Breaking: #69227 - Strings for like are not properly escaped
+============================================================
+
+Description
+===========
+
+The following constants are removed:
+
+* ``\TYPO3\CMS\IndexedSearch\Controller\SearchFormController::WILDCARD_LEFT``
+* ``\TYPO3\CMS\IndexedSearch\Controller\SearchFormController::WILDCARD_RIGHT``
+* ``\TYPO3\CMS\IndexedSearch\Domain\Repository\IndexSearchRepository::WILDCARD_LEFT``
+* ``\TYPO3\CMS\IndexedSearch\Domain\Repository\IndexSearchRepository::WILDCARD_RIGHT``
+
+
+Impact
+======
+
+The mentioned constants don't exist anymore.
+
+
+Affected Installations
+======================
+
+Third party code using the mentioned constants
+
+
+Migration
+=========
+
+Use the new constants:
+
+* ``\TYPO3\CMS\IndexedSearch\Utility\LikeWildcard::LEFT``
+* ``\TYPO3\CMS\IndexedSearch\Utility\LikeWildcard::RIGHT``
\ No newline at end of file
index 4b42ec0..003c4b8 100755 (executable)
@@ -16,6 +16,7 @@ namespace TYPO3\CMS\IndexedSearch\Controller;
 
 use TYPO3\CMS\Core\Html\HtmlParser;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\IndexedSearch\Utility;
 
 /**
  * Index search frontend
@@ -169,8 +170,6 @@ class SearchFormController extends \TYPO3\CMS\Frontend\Plugin\AbstractPlugin {
         */
        public $lexerObj;
 
-       const WILDCARD_LEFT = 1;
-       const WILDCARD_RIGHT = 2;
        /**
         * Main function, called from TypoScript as a USER_INT object.
         *
@@ -807,15 +806,15 @@ class SearchFormController extends \TYPO3\CMS\Frontend\Plugin\AbstractPlugin {
                        switch ($theType) {
                                case '1':
                                        // Part of word
-                                       $res = $this->searchWord($sWord, self::WILDCARD_LEFT | self::WILDCARD_RIGHT);
+                                       $res = $this->searchWord($sWord, Utility\LikeWildcard::BOTH);
                                        break;
                                case '2':
                                        // First part of word
-                                       $res = $this->searchWord($sWord, self::WILDCARD_RIGHT);
+                                       $res = $this->searchWord($sWord, Utility\LikeWildcard::RIGHT);
                                        break;
                                case '3':
                                        // Last part of word
-                                       $res = $this->searchWord($sWord, self::WILDCARD_LEFT);
+                                       $res = $this->searchWord($sWord, Utility\LikeWildcard::LEFT);
                                        break;
                                case '10':
                                        // Sounds like
@@ -892,13 +891,17 @@ class SearchFormController extends \TYPO3\CMS\Frontend\Plugin\AbstractPlugin {
         * Search for a word
         *
         * @param string $sWord Word to search for
-        * @param int $mode Bit-field which can contain WILDCARD_LEFT and/or WILDCARD_RIGHT
+        * @param int $wildcard Bit-field of Utility\LikeWildcard
         * @return bool|\mysqli_result SQL result pointer
         */
-       public function searchWord($sWord, $mode) {
-               $wildcard_left = $mode & self::WILDCARD_LEFT ? '%' : '';
-               $wildcard_right = $mode & self::WILDCARD_RIGHT ? '%' : '';
-               $wSel = 'IW.baseword LIKE \'' . $wildcard_left . $this->databaseConnection->quoteStr($sWord, 'index_words') . $wildcard_right . '\'';
+       public function searchWord($sWord, $wildcard) {
+               $likeWildcard = Utility\LikeWildcard::cast($wildcard);
+               $wSel = $likeWildcard->getLikeQueryPart(
+                       'index_words',
+                       'IW.baseword',
+                       $sWord
+               );
+
                $this->wSelClauses[] = $wSel;
                $res = $this->execPHashListQuery($wSel, ' AND is_stopword=0');
                return $res;
@@ -924,11 +927,18 @@ class SearchFormController extends \TYPO3\CMS\Frontend\Plugin\AbstractPlugin {
         * @return bool|\mysqli_result SQL result pointer
         */
        public function searchSentence($sSentence) {
-               $res = $this->databaseConnection->exec_SELECTquery('ISEC.phash', 'index_section ISEC, index_fulltext IFT', 'IFT.fulltextdata LIKE \'%' . $this->databaseConnection->quoteStr($sSentence, 'index_fulltext') . '%\' AND
-                               ISEC.phash = IFT.phash
-                       ' . $this->sectionTableWhere(), 'ISEC.phash');
                $this->wSelClauses[] = '1=1';
-               return $res;
+               $likeWildcard = Utility\LikeWildcard::cast(Utility\LikeWildcard::BOTH);
+               $likePart = $likeWildcard->getLikeQueryPart(
+                       'index_fulltext',
+                       'IFT.fulltextdata',
+                       $sSentence
+               );
+
+               return $this->databaseConnection->exec_SELECTquery('ISEC.phash',
+                       'index_section ISEC, index_fulltext IFT',
+                       $likePart . ' AND ISEC.phash = IFT.phash' . $this->sectionTableWhere(), 'ISEC.phash'
+               );
        }
 
        /**
index f2a3d78..55a7e7d 100644 (file)
@@ -17,7 +17,7 @@ namespace TYPO3\CMS\IndexedSearch\Domain\Repository;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Core\Utility\MathUtility;
 use TYPO3\CMS\IndexedSearch\Indexer;
-use TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility;
+use TYPO3\CMS\IndexedSearch\Utility;
 
 /**
  * Index search abstraction to search through the index
@@ -156,10 +156,6 @@ class IndexSearchRepository {
         */
        protected $displayForbiddenRecords = FALSE;
 
-       // constants to help where to use wildcards in SQL like queries
-       const WILDCARD_LEFT = 1;
-       const WILDCARD_RIGHT = 2;
-
        /**
         * initialize all options that are necessary for the search
         *
@@ -345,15 +341,15 @@ class IndexSearchRepository {
                        switch ($theType) {
                                case '1':
                                        // Part of word
-                                       $res = $this->searchWord($sWord, self::WILDCARD_LEFT | self::WILDCARD_RIGHT);
+                                       $res = $this->searchWord($sWord, Utility\LikeWildcard::BOTH);
                                        break;
                                case '2':
                                        // First part of word
-                                       $res = $this->searchWord($sWord, self::WILDCARD_RIGHT);
+                                       $res = $this->searchWord($sWord, Utility\LikeWildcard::RIGHT);
                                        break;
                                case '3':
                                        // Last part of word
-                                       $res = $this->searchWord($sWord, self::WILDCARD_LEFT);
+                                       $res = $this->searchWord($sWord, Utility\LikeWildcard::LEFT);
                                        break;
                                case '10':
                                        // Sounds like
@@ -434,13 +430,16 @@ class IndexSearchRepository {
         * Search for a word
         *
         * @param string $sWord the search word
-        * @param int $mode constant from this class to see if the wildcard should be left and/or right of the search string
+        * @param int $wildcard Bit-field of Utility\LikeWildcard
         * @return bool|\mysqli_result SQL result pointer
         */
-       protected function searchWord($sWord, $mode) {
-               $wildcard_left = $mode & self::WILDCARD_LEFT ? '%' : '';
-               $wildcard_right = $mode & self::WILDCARD_RIGHT ? '%' : '';
-               $wSel = 'IW.baseword LIKE \'' . $wildcard_left . $this->getDatabaseConnection()->quoteStr($sWord, 'index_words') . $wildcard_right . '\'';
+       protected function searchWord($sWord, $wildcard) {
+               $likeWildcard = Utility\LikeWildcard::cast($wildcard);
+               $wSel = $likeWildcard->getLikeQueryPart(
+                       'index_words',
+                       'IW.baseword',
+                       $sWord
+               );
                $this->wSelClauses[] = $wSel;
                return $this->execPHashListQuery($wSel, ' AND is_stopword=0');
        }
@@ -465,16 +464,17 @@ class IndexSearchRepository {
         */
        protected function searchSentence($sWord) {
                $this->wSelClauses[] = '1=1';
-               $sWord = $this->getDatabaseConnection()->quoteStr(
-                       $this->getDatabaseConnection()->escapeStrForLike($sWord, 'index_fulltext'),
-                       'index_fulltext'
+               $likeWildcard = Utility\LikeWildcard::cast(Utility\LikeWildcard::BOTH);
+               $likePart = $likeWildcard->getLikeQueryPart(
+                       'index_fulltext',
+                       'IFT.fulltextdata',
+                       $sWord
                );
+
                return $this->getDatabaseConnection()->exec_SELECTquery(
                        'ISEC.phash',
                        'index_section ISEC, index_fulltext IFT',
-                       'IFT.fulltextdata LIKE \'%' . $sWord
-                               . '%\' AND ISEC.phash = IFT.phash'
-                               . $this->sectionTableWhere(),
+                       $likePart . ' AND ISEC.phash = IFT.phash' . $this->sectionTableWhere(),
                        'ISEC.phash'
                );
        }
@@ -839,7 +839,7 @@ class IndexSearchRepository {
         * @return int Integer intepretation of the md5 hash of input string.
         */
        protected function md5inthash($str) {
-               return IndexedSearchUtility::md5inthash($str);
+               return Utility\IndexedSearchUtility::md5inthash($str);
        }
 
        /**
@@ -851,7 +851,7 @@ class IndexSearchRepository {
         * @return bool TRUE if given tables are enabled
         */
        protected function isTableUsed($table_list) {
-               return IndexedSearchUtility::isTableUsed($table_list);
+               return Utility\IndexedSearchUtility::isTableUsed($table_list);
        }
 
        /**
diff --git a/typo3/sysext/indexed_search/Classes/Utility/LikeWildcard.php b/typo3/sysext/indexed_search/Classes/Utility/LikeWildcard.php
new file mode 100644 (file)
index 0000000..91af8bf
--- /dev/null
@@ -0,0 +1,59 @@
+<?php
+namespace TYPO3\CMS\IndexedSearch\Utility;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+/**
+ * Enumeration object for LikeWildcard
+ */
+class LikeWildcard extends \TYPO3\CMS\Core\Type\Enumeration {
+
+       const __default = self::BOTH;
+
+       /** @var int Do not use any wildcard */
+       const NONE = 0;
+
+       /** @var int Use wildcard on left side */
+       const LEFT = 1;
+
+       /** @var int Use wildcard on right side */
+       const RIGHT = 2;
+
+       /** @var int Use wildcard on both sides */
+       const BOTH = 3;
+
+       /**
+        * Returns a LIKE clause for sql queries.
+        *
+        * @param string $tableName The name of the table to query.
+        * @param string $fieldName The name of the field to query with LIKE.
+        * @param string $likeValue The value for the LIKE clause operation.
+        * @return string
+        * @throws \TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException
+        */
+       public function getLikeQueryPart($tableName, $fieldName, $likeValue) {
+               $databaseConnection = $GLOBALS['TYPO3_DB'];
+
+               $likeValue = $databaseConnection->quoteStr(
+                       $databaseConnection->escapeStrForLike($likeValue, $tableName),
+                       $tableName
+               );
+
+               return $fieldName . ' LIKE \''
+                       . ($this->value & self::LEFT ? '%' : '')
+                       . $likeValue
+                       . ($this->value & self::RIGHT ? '%' : '')
+                       . '\'';
+       }
+}
diff --git a/typo3/sysext/indexed_search/Tests/Unit/Utility/LikeWildcardTest.php b/typo3/sysext/indexed_search/Tests/Unit/Utility/LikeWildcardTest.php
new file mode 100644 (file)
index 0000000..fc01eb5
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+namespace TYPO3\CMS\IndexedSearch\Tests\Unit\Utility;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+use TYPO3\CMS\IndexedSearch\Utility\LikeWildcard;
+
+/**
+ * This class contains unit tests for the LikeQueryUtility
+ */
+class LikeWildcardTest extends \TYPO3\CMS\Core\Tests\UnitTestCase {
+
+       /**
+        * Sets up this test case.
+        */
+       protected function setUp() {
+               /** @var $databaseConnectionMock \TYPO3\CMS\Core\Database\DatabaseConnection|\PHPUnit_Framework_MockObject_MockObject */
+               $databaseConnectionMock = $this->getMock(\TYPO3\CMS\Core\Database\DatabaseConnection::class, array('quoteStr'));
+               $databaseConnectionMock->method('quoteStr')
+                       ->will($this->returnArgument(0));
+               $GLOBALS['TYPO3_DB'] = $databaseConnectionMock;
+       }
+
+       /**
+        * @test
+        * @param string $tableName
+        * @param string $fieldName
+        * @param string $likeValue
+        * @param int $wildcard
+        * @param string $expected
+        * @dataProvider getLikeQueryPartDataProvider
+        */
+       public function getLikeQueryPart($tableName, $fieldName, $likeValue, $wildcard, $expected) {
+               $subject = \TYPO3\CMS\IndexedSearch\Utility\LikeWildcard::cast($wildcard);
+               $this->assertSame($expected, $subject->getLikeQueryPart($tableName, $fieldName, $likeValue));
+       }
+
+       /**
+        * Returns data sets for the test getLikeQueryPart
+        * Each dataset is an array with the following elements:
+        * - the table name
+        * - the field name
+        * - the search value
+        * - the wildcard mode
+        * - the expected result
+        *
+        * @return array
+        */
+       public function getLikeQueryPartDataProvider() {
+               return [
+                       'no placeholders and no wildcard mode' => [
+                               'tt_content',
+                               'body',
+                               'searchstring',
+                               LikeWildcard::NONE,
+                               'body LIKE \'searchstring\''
+                       ],
+                       'no placeholders and left wildcard mode' => [
+                               'tt_content',
+                               'body',
+                               'searchstring',
+                               LikeWildcard::LEFT,
+                               'body LIKE \'%searchstring\''
+                       ],
+                       'no placeholders and right wildcard mode' => [
+                               'tt_content',
+                               'body',
+                               'searchstring',
+                               LikeWildcard::RIGHT,
+                               'body LIKE \'searchstring%\''
+                       ],
+                       'no placeholders and both wildcards mode' => [
+                               'tt_content',
+                               'body',
+                               'searchstring',
+                               LikeWildcard::BOTH,
+                               'body LIKE \'%searchstring%\''
+                       ],
+                       'underscore placeholder and left wildcard mode' => [
+                               'tt_content',
+                               'body',
+                               'search_string',
+                               LikeWildcard::LEFT,
+                               'body LIKE \'%search\\_string\''
+                       ],
+                       'percent placeholder and right wildcard mode' => [
+                               'tt_content',
+                               'body',
+                               'search%string',
+                               LikeWildcard::RIGHT,
+                               'body LIKE \'search\\%string%\''
+                       ],
+                       'percent and underscore placeholder and both wildcards mode' => [
+                               'tt_content',
+                               'body',
+                               '_search%string_',
+                               LikeWildcard::RIGHT,
+                               'body LIKE \'\\_search\\%string\\_%\''
+                       ],
+               ];
+       }
+}