[!!!][TASK] Make TimeTracker a singleton
[Packages/TYPO3.CMS.git] / typo3 / sysext / indexed_search_mysql / Classes / Hook / MysqlFulltextIndexHook.php
1 <?php
2 namespace TYPO3\CMS\IndexedSearchMysql\Hook;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
17 use TYPO3\CMS\Core\Utility\GeneralUtility;
18
19 /**
20 * Class that hooks into Indexed Search and replaces standard SQL queries with MySQL fulltext index queries.
21 */
22 class MysqlFulltextIndexHook
23 {
24 /**
25 * @var \TYPO3\CMS\IndexedSearch\Controller\SearchFormController|\TYPO3\CMS\IndexedSearch\Domain\Repository\IndexSearchRepository
26 */
27 public $pObj;
28
29 const ANY_PART_OF_THE_WORD = '1';
30 const LAST_PART_OF_THE_WORD = '2';
31 const FIRST_PART_OF_THE_WORD = '3';
32 const SOUNDS_LIKE = '10';
33 const SENTENCE = '20';
34 /**
35 * Gets a SQL result pointer to traverse for the search records.
36 *
37 * @param array $searchWordsArray Search words
38 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
39 * @return bool|\mysqli_result|object MySQLi result object / DBAL object
40 */
41 public function getResultRows_SQLpointer($searchWordsArray, $freeIndexUid = -1)
42 {
43 // Build the search string, detect which fulltext index to use, and decide whether boolean search is needed or not
44 $searchData = $this->getSearchString($searchWordsArray);
45 // Perform SQL Search / collection of result rows array:
46 $resource = false;
47 if ($searchData) {
48 /** @var TimeTracker $timeTracker */
49 $timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
50 // Do the search:
51 $timeTracker->push('execFinalQuery');
52 $resource = $this->execFinalQuery_fulltext($searchData, $freeIndexUid);
53 $timeTracker->pull();
54 }
55 return $resource;
56 }
57
58 /**
59 * Returns a search string for use with MySQL FULLTEXT query
60 *
61 * @param array $searchWordArray Search word array
62 * @return string Search string
63 */
64 public function getSearchString($searchWordArray)
65 {
66 // Initialize variables:
67 $count = 0;
68 // Change this to TRUE to force BOOLEAN SEARCH MODE (useful if fulltext index is still empty)
69 $searchBoolean = false;
70 $fulltextIndex = 'index_fulltext.fulltextdata';
71 // This holds the result if the search is natural (doesn't contain any boolean operators)
72 $naturalSearchString = '';
73 // This holds the result if the search is boolen (contains +/-/| operators)
74 $booleanSearchString = '';
75
76 $searchType = (string)$this->pObj->getSearchType();
77
78 // Traverse searchwords and prefix them with corresponding operator
79 foreach ($searchWordArray as $searchWordData) {
80 // Making the query for a single search word based on the search-type
81 $searchWord = $searchWordData['sword'];
82 $wildcard = '';
83 if (strstr($searchWord, ' ')) {
84 $searchType = self::SENTENCE;
85 }
86 switch ($searchType) {
87 case self::ANY_PART_OF_THE_WORD:
88
89 case self::LAST_PART_OF_THE_WORD:
90
91 case self::FIRST_PART_OF_THE_WORD:
92 // First part of word
93 $wildcard = '*';
94 // Part-of-word search requires boolean mode!
95 $searchBoolean = true;
96 break;
97 case self::SOUNDS_LIKE:
98 $indexerObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\IndexedSearch\Indexer::class);
99 // Initialize the indexer-class
100 /** @var \TYPO3\CMS\IndexedSearch\Indexer $indexerObj */
101 $searchWord = $indexerObj->metaphone($searchWord, $indexerObj->storeMetaphoneInfoAsWords);
102 unset($indexerObj);
103 $fulltextIndex = 'index_fulltext.metaphonedata';
104 break;
105 case self::SENTENCE:
106 $searchBoolean = true;
107 // Remove existing quotes and fix misplaced quotes.
108 $searchWord = trim(str_replace('"', ' ', $searchWord));
109 break;
110 }
111 // Perform search for word:
112 switch ($searchWordData['oper']) {
113 case 'AND NOT':
114 $booleanSearchString .= ' -' . $searchWord . $wildcard;
115 $searchBoolean = true;
116 break;
117 case 'OR':
118 $booleanSearchString .= ' ' . $searchWord . $wildcard;
119 $searchBoolean = true;
120 break;
121 default:
122 $booleanSearchString .= ' +' . $searchWord . $wildcard;
123 $naturalSearchString .= ' ' . $searchWord;
124 }
125 $count++;
126 }
127 if ($searchType == self::SENTENCE) {
128 $searchString = '"' . trim($naturalSearchString) . '"';
129 } elseif ($searchBoolean) {
130 $searchString = trim($booleanSearchString);
131 } else {
132 $searchString = trim($naturalSearchString);
133 }
134 return array(
135 'searchBoolean' => $searchBoolean,
136 'searchString' => $searchString,
137 'fulltextIndex' => $fulltextIndex
138 );
139 }
140
141 /**
142 * Execute final query, based on phash integer list. The main point is sorting the result in the right order.
143 *
144 * @param array $searchData Array with search string, boolean indicator, and fulltext index reference
145 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
146 * @return bool|\mysqli_result|object MySQLi result object / DBAL object
147 */
148 protected function execFinalQuery_fulltext($searchData, $freeIndexUid = -1)
149 {
150 // Setting up methods of filtering results based on page types, access, etc.
151 $pageJoin = '';
152 // Indexing configuration clause:
153 $freeIndexUidClause = $this->pObj->freeIndexUidWhere($freeIndexUid);
154 // Calling hook for alternative creation of page ID list
155 $searchRootPageIdList = $this->pObj->getSearchRootPageIdList();
156 if ($hookObj = &$this->pObj->hookRequest('execFinalQuery_idList')) {
157 $pageWhere = $hookObj->execFinalQuery_idList('');
158 } elseif ($this->pObj->getJoinPagesForQuery()) {
159 // Alternative to getting all page ids by ->getTreeList() where "excludeSubpages" is NOT respected.
160 $pageJoin = ',
161 pages';
162 $pageWhere = 'pages.uid = ISEC.page_id
163 ' . $GLOBALS['TSFE']->cObj->enableFields('pages') . '
164 AND pages.no_search=0
165 AND pages.doktype<200
166 ';
167 } elseif ($searchRootPageIdList[0] >= 0) {
168
169 // Collecting all pages IDs in which to search;
170 // filtering out ALL pages that are not accessible due to enableFields. Does NOT look for "no_search" field!
171 $idList = array();
172 foreach ($searchRootPageIdList as $rootId) {
173 /** @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $cObj */
174 $cObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class);
175 $idList[] = $cObj->getTreeList(-1 * $rootId, 9999);
176 }
177 $pageWhere = ' ISEC.page_id IN (' . implode(',', $idList) . ')';
178 } else {
179 // Disable everything... (select all)
180 $pageWhere = ' 1=1';
181 }
182 $searchBoolean = '';
183 if ($searchData['searchBoolean']) {
184 $searchBoolean = ' IN BOOLEAN MODE';
185 }
186 $resource = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
187 'index_fulltext.*, ISEC.*, IP.*',
188 'index_fulltext, index_section ISEC, index_phash IP' . $pageJoin,
189 'MATCH (' . $searchData['fulltextIndex'] . ')
190 AGAINST (' . $GLOBALS['TYPO3_DB']->fullQuoteStr($searchData['searchString'], 'index_fulltext') . $searchBoolean . ') ' .
191 $this->pObj->mediaTypeWhere() . ' ' . $this->pObj->languageWhere() . $freeIndexUidClause . '
192 AND index_fulltext.phash = IP.phash
193 AND ISEC.phash = IP.phash
194 AND ' . $pageWhere . $this->pObj->sectionTableWhere(),
195 'IP.phash,ISEC.phash,ISEC.phash_t3,ISEC.rl0,ISEC.rl1,ISEC.rl2,ISEC.page_id,ISEC.uniqid,IP.phash_grouping,IP.data_filename ,IP.data_page_id ,IP.data_page_reg1,IP.data_page_type,IP.data_page_mp,IP.gr_list,IP.item_type,IP.item_title,IP.item_description,IP.item_mtime,IP.tstamp,IP.item_size,IP.contentHash,IP.crdate,IP.parsetime,IP.sys_language_uid,IP.item_crdate,IP.cHashParams,IP.externalUrl,IP.recordUid,IP.freeIndexUid,IP.freeIndexSetId'
196 );
197 return $resource;
198 }
199 }