Passed
Push — release-11.5.x ( eab588...506b54 )
by Rafael
49:07 queued 25:53
created

isSerializedResultFromCustomContentElement()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 20
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 20
ccs 9
cts 9
cp 1
rs 9.6111
c 0
b 0
f 0
cc 5
nc 8
nop 2
crap 5
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace ApacheSolrForTypo3\Solr\IndexQueue;
19
20
use ApacheSolrForTypo3\Solr\ContentObject\Classification;
21
use ApacheSolrForTypo3\Solr\ContentObject\Multivalue;
22
use ApacheSolrForTypo3\Solr\ContentObject\Relation;
23
use ApacheSolrForTypo3\Solr\System\Solr\Document\Document;
24
use TYPO3\CMS\Core\Core\Environment;
25
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
26
use TYPO3\CMS\Core\Utility\GeneralUtility;
27
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
28
use UnexpectedValueException;
29
30
/**
31
 * An abstract indexer class to collect a few common methods shared with other
32
 * indexers.
33
 *
34
 * @author Ingo Renner <[email protected]>
35
 */
36
abstract class AbstractIndexer
37
{
38
    /**
39
     * Holds the type of the data to be indexed, usually that is the table name.
40
     *
41
     * @var string
42
     */
43
    protected string $type = '';
44
45
    /**
46
     * Holds field names that are denied to overwrite in thy indexing configuration.
47
     *
48
     * @var array
49
     */
50
    protected static array $unAllowedOverrideFields = ['type'];
51
52
    /**
53
     * @param string $solrFieldName
54
     * @return bool
55
     */
56 62
    public static function isAllowedToOverrideField(string $solrFieldName): bool
57
    {
58 62
        return !in_array($solrFieldName, static::$unAllowedOverrideFields);
59
    }
60
61
    /**
62
     * Adds fields to the document as defined in $indexingConfiguration
63
     *
64
     * @param Document $document base document to add fields to
65
     * @param array $indexingConfiguration Indexing configuration / mapping
66
     * @param array $data Record data
67
     * @return Document Modified document with added fields
68
     */
69 20
    protected function addDocumentFieldsFromTyposcript(Document $document, array $indexingConfiguration, array $data, TypoScriptFrontendController $tsfe): Document
70
    {
71 20
        $data = static::addVirtualContentFieldToRecord($document, $data);
72
73
        // mapping of record fields => solr document fields, resolving cObj
74 20
        foreach ($indexingConfiguration as $solrFieldName => $recordFieldName) {
75 20
            if (is_array($recordFieldName)) {
76
                // configuration for a content object, skipping
77 16
                continue;
78
            }
79
80 20
            if (!static::isAllowedToOverrideField($solrFieldName)) {
81
                throw new InvalidFieldNameException(
82
                    'Must not overwrite field .' . $solrFieldName,
83
                    1435441863
84
                );
85
            }
86
87 20
            $fieldValue = $this->resolveFieldValue($indexingConfiguration, $solrFieldName, $data, $tsfe);
88
89 20
            if (is_array($fieldValue)) {
90
                // multi value
91 9
                $document->setField($solrFieldName, $fieldValue);
92
            } else {
93 20
                if ($fieldValue !== '' && $fieldValue !== null) {
94 19
                    $document->setField($solrFieldName, $fieldValue);
95
                }
96
            }
97
        }
98
99 20
        return $document;
100
    }
101
102
    /**
103
     * Adds the content of the field 'content' from the solr document as virtual field __solr_content in the record,
104
     * to have it available in typoscript.
105
     *
106
     * @param Document $document
107
     * @param array $data
108
     * @return array
109
     */
110 28
    public static function addVirtualContentFieldToRecord(Document $document, array $data): array
111
    {
112 28
        if (isset($document['content'])) {
113 8
            $data['__solr_content'] = $document['content'];
114 8
            return $data;
115
        }
116 20
        return $data;
117
    }
118
119
    /**
120
     * Resolves a field to its value depending on its configuration.
121
     *
122
     * This enables you to configure the indexer to put the item/record through
123
     * cObj processing if wanted/needed. Otherwise, the plain item/record value
124
     * is taken.
125
     *
126
     * @param array $indexingConfiguration Indexing configuration as defined in plugin.tx_solr_index.queue.[indexingConfigurationName].fields
127
     * @param string $solrFieldName A Solr field name that is configured in the indexing configuration
128
     * @param array $data A record or item's data
129
     * @param TypoScriptFrontendController $tsfe
130
     * @return string The resolved string value to be indexed
131
     */
132 20
    protected function resolveFieldValue(
133
        array $indexingConfiguration,
134
        string $solrFieldName,
135
        array $data,
136
        TypoScriptFrontendController $tsfe
137
    ) {
138 20
        if (isset($indexingConfiguration[$solrFieldName . '.'])) {
139
            // configuration found => need to resolve a cObj
140
141
            // need to change directory to make IMAGE content objects work in BE context
142
            // see http://blog.netzelf.de/lang/de/tipps-und-tricks/tslib_cobj-image-im-backend
143 16
            $backupWorkingDirectory = getcwd();
144 16
            chdir(Environment::getPublicPath() . '/');
145
146 16
            $tsfe->cObj->start($data, $this->type);
147 16
            $fieldValue = $tsfe->cObj->cObjGetSingle(
148 16
                $indexingConfiguration[$solrFieldName],
149 16
                $indexingConfiguration[$solrFieldName . '.']
150
            );
151
152 16
            chdir($backupWorkingDirectory);
153
154 16
            if ($this->isSerializedValue(
155
                $indexingConfiguration,
156
                $solrFieldName
157
            )
158
            ) {
159 16
                $fieldValue = unserialize($fieldValue);
160
            }
161
        } elseif (
162 19
            substr($indexingConfiguration[$solrFieldName], 0, 1) === '<'
163
        ) {
164
            $referencedTsPath = trim(substr(
165
                $indexingConfiguration[$solrFieldName],
166
                1
167
            ));
168
            $typoScriptParser = GeneralUtility::makeInstance(TypoScriptParser::class);
169
            // $name and $conf is loaded with the referenced values.
170
            list($name, $conf) = $typoScriptParser->getVal($referencedTsPath, $GLOBALS['TSFE']->tmpl->setup);
171
172
            // need to change directory to make IMAGE content objects work in BE context
173
            // see http://blog.netzelf.de/lang/de/tipps-und-tricks/tslib_cobj-image-im-backend
174
            $backupWorkingDirectory = getcwd();
175
            chdir(Environment::getPublicPath() . '/');
176
177
            $tsfe->cObj->start($data, $this->type);
178
            $fieldValue = $tsfe->cObj->cObjGetSingle($name, $conf);
179
180
            chdir($backupWorkingDirectory);
181
182
            if ($this->isSerializedValue(
183
                $indexingConfiguration,
184
                $solrFieldName
185
            )
186
            ) {
187
                $fieldValue = unserialize($fieldValue);
188
            }
189
        } else {
190 19
            $fieldValue = $data[$indexingConfiguration[$solrFieldName]];
191
        }
192
193
        // detect and correct type for dynamic fields
194
195
        // find last underscore, substr from there, cut off last character (S/M)
196 20
        $fieldType = substr(
197
            $solrFieldName,
198 20
            strrpos($solrFieldName, '_') + 1,
199
            -1
200
        );
201 20
        if (is_array($fieldValue)) {
202 9
            foreach ($fieldValue as $key => $value) {
203 9
                $fieldValue[$key] = $this->ensureFieldValueType(
204
                    $value,
205
                    $fieldType
206
                );
207
            }
208
        } else {
209 20
            $fieldValue = $this->ensureFieldValueType($fieldValue, $fieldType);
210
        }
211
212 20
        return $fieldValue;
213
    }
214
215
    // Utility methods
216
217
    /**
218
     * Uses a field's configuration to detect whether its value returned by a
219
     * content object is expected to be serialized and thus needs to be
220
     * unserialized.
221
     *
222
     * @param array $indexingConfiguration Current item's indexing configuration
223
     * @param string $solrFieldName Current field being indexed
224
     * @return bool TRUE if the value is expected to be serialized, FALSE otherwise
225
     */
226 27
    public static function isSerializedValue(array $indexingConfiguration, string $solrFieldName): bool
227
    {
228 27
        return static::isSerializedResultFromRegisteredHook($indexingConfiguration, $solrFieldName)
229 26
            || static::isSerializedResultFromCustomContentElement($indexingConfiguration, $solrFieldName);
230
    }
231
232
    /**
233
     * Checks if the response comes from a custom content element that returns a serialized value.
234
     *
235
     * @param array $indexingConfiguration
236
     * @param string $solrFieldName
237
     * @return bool
238
     */
239 25
    protected static function isSerializedResultFromCustomContentElement(array $indexingConfiguration, string $solrFieldName): bool
240
    {
241 25
        $isSerialized = false;
242
243
        // SOLR_CLASSIFICATION - always returns serialized array
244 25
        if (($indexingConfiguration[$solrFieldName] ?? null) == Classification::CONTENT_OBJECT_NAME) {
245 1
            $isSerialized = true;
246
        }
247
248
        // SOLR_MULTIVALUE - always returns serialized array
249 25
        if (($indexingConfiguration[$solrFieldName] ?? null) == Multivalue::CONTENT_OBJECT_NAME) {
250 2
            $isSerialized = true;
251
        }
252
253
        // SOLR_RELATION - returns serialized array if multiValue option is set
254 25
        if (($indexingConfiguration[$solrFieldName] ?? null) == Relation::CONTENT_OBJECT_NAME && !empty($indexingConfiguration[$solrFieldName . '.']['multiValue'])) {
255 12
            $isSerialized = true;
256
        }
257
258 25
        return $isSerialized;
259
    }
260
261
    /**
262
     * Checks registered hooks if a SerializedValueDetector detects a serialized response.
263
     *
264
     * @param array $indexingConfiguration
265
     * @param string $solrFieldName
266
     * @return bool
267
     */
268 27
    protected static function isSerializedResultFromRegisteredHook(array $indexingConfiguration, string $solrFieldName): bool
269
    {
270 27
        if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['detectSerializedValue'] ?? null)) {
271 24
            return false;
272
        }
273
274 3
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['detectSerializedValue'] as $classReference) {
275 2
            $serializedValueDetector = GeneralUtility::makeInstance($classReference);
276 2
            if (!$serializedValueDetector instanceof SerializedValueDetector) {
277 1
                $message = get_class($serializedValueDetector) . ' must implement interface ' . SerializedValueDetector::class;
278 1
                throw new UnexpectedValueException($message, 1404471741);
279
            }
280
281 1
            $isSerialized = (boolean)$serializedValueDetector->isSerializedValue($indexingConfiguration, $solrFieldName);
282 1
            if ($isSerialized) {
283 1
                return true;
284
            }
285
        }
286 1
        return false;
287
    }
288
289
    /**
290
     * Makes sure a field's value matches a (dynamic) field's type.
291
     *
292
     * @param mixed $value Value to be added to a document
293
     * @param string $fieldType The dynamic field's type
294
     * @return mixed Returns the value in the correct format for the field type
295
     */
296 20
    protected function ensureFieldValueType($value, string $fieldType)
297
    {
298 20
        switch ($fieldType) {
299 20
            case 'int':
300 20
            case 'tInt':
301
                $value = (int)$value;
302
                break;
303
304 20
            case 'float':
305 20
            case 'tFloat':
306
                $value = (float)$value;
307
                break;
308 20
            case 'long':
309
                // long and double do not exist in PHP
310
                // simply make sure it somehow looks like a number
311
                // <insert PHP rant here>
312 20
            case 'tLong':
313
                // remove anything that's not a number or negative/minus sign
314
                $value = preg_replace('/[^0-9\\-]/', '', $value);
315
                if (trim($value) === '') {
316
                    $value = 0;
317
                }
318
                break;
319 20
            case 'double':
320 20
            case 'tDouble':
321 20
            case 'tDouble4':
322
                // as long as it's numeric we'll take it, int or float doesn't matter
323
                if (!is_numeric($value)) {
324
                    $value = 0;
325
                }
326
                break;
327
328
            default:
329
            // assume things are correct for non-dynamic fields
330
            }
331
332 20
        return $value;
333
    }
334
}
335