Completed
Push — 2.0 ( 45f860...1c5343 )
by Christopher
02:36
created

GenericEngine::_isFullTextEnabled()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 34
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 19
nc 6
nop 0
dl 0
loc 34
rs 6.7272
c 0
b 0
f 0
1
<?php
2
/**
3
 * Licensed under The GPL-3.0 License
4
 * For full copyright and license information, please see the LICENSE.txt
5
 * Redistributions of files must retain the above copyright notice.
6
 *
7
 * @since    2.0.0
8
 * @author   Christopher Castro <[email protected]>
9
 * @link     http://www.quickappscms.org
10
 * @license  http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License
11
 */
12
namespace Search\Engine\Generic;
13
14
use Cake\Cache\Cache;
15
use Cake\Core\InstanceConfigTrait;
16
use Cake\Datasource\EntityInterface;
17
use Cake\Error\FatalErrorException;
18
use Cake\Event\Event;
19
use Cake\Event\EventManager;
20
use Cake\ORM\Query;
21
use Cake\ORM\Table;
22
use Cake\ORM\TableRegistry;
23
use Cake\Utility\Hash;
24
use Cake\Utility\Inflector;
25
use Search\Engine\BaseEngine;
26
use Search\Engine\Generic\Exception\CompoundPrimaryKeyException;
27
use Search\Parser\MiniLanguage\MiniLanguageParser;
28
use Search\Parser\TokenInterface;
29
use \ArrayObject;
30
31
/**
32
 * This Search Engine allows entities to be searchable through an auto-generated
33
 * list of words.
34
 *
35
 * ## Using Generic Engine
36
 *
37
 * You must indicate Searchable behavior to use this engine, for example when
38
 * attaching Searchable behavior to `Articles` table:
39
 *
40
 * ```php
41
 * $this->addBehavior('Search.Searchable', [
42
 *     'engine' => [
43
 *         'className' => 'Search\Engine\Generic\GenericEngine',
44
 *         'config' => [
45
 *             'bannedWords' => []
46
 *         ]
47
 *     ]
48
 * ]);
49
 * ```
50
 *
51
 * This engine will apply a series of filters (converts to lowercase, remove line
52
 * breaks, etc) to words list extracted from each entity being indexed.
53
 *
54
 * ### Banned Words
55
 *
56
 * You can use the `bannedWords` option to tell which words should not be indexed by
57
 * this engine. For example:
58
 *
59
 * ```php
60
 * $this->addBehavior('Search.Searchable', [
61
 *     'engine' => [
62
 *         'className' => 'Search\Engine\Generic\GenericEngine',
63
 *         'config' => [
64
 *             'bannedWords' => ['of', 'the', 'and']
65
 *         ]
66
 *     ]
67
 * ]);
68
 * ```
69
 *
70
 * If you need to ban a really specific list of words you can set `bannedWords`
71
 * option as a callable method that should return true or false to tell if a words
72
 * should be indexed or not. For example:
73
 *
74
 * ```php
75
 * $this->addBehavior('Search.Searchable', [
76
 *     'engine' => [
77
 *         'className' => 'Search\Engine\Generic\GenericEngine',
78
 *         'config' => [
79
 *             'bannedWords' => function ($word) {
80
 *                 return strlen($word) > 3;
81
 *             }
82
 *         ]
83
 *     ]
84
 * ]);
85
 * ```
86
 *
87
 * - Returning TRUE indicates that the word is safe for indexing (not banned).
88
 * - Returning FALSE indicates that the word should NOT be indexed (banned).
89
 *
90
 * In the example, above any word of 4 or more characters will be indexed
91
 * (e.g. "home", "name", "quickapps", etc). Any word of 3 or less characters will
92
 * be banned (e.g. "and", "or", "the").
93
 *
94
 * ## Searching Entities
95
 *
96
 * When using this engine, every entity under your table gets a list of indexed
97
 * words. The idea behind this is that you can use this list of words to locate any
98
 * entity based on a customized search-criteria. A search-criteria looks as follow:
99
 *
100
 *     "this phrase" OR -"not this one" AND this
101
 *
102
 * ---
103
 *
104
 * Use wildcard searches to broaden results; asterisk (`*`) matches any one or
105
 * more characters, exclamation mark (`!`) matches any single character:
106
 *
107
 *     "this *rase" OR -"not th!! one" AND thi!
108
 *
109
 * Anything containing space (" ") characters must be wrapper between quotation
110
 * marks:
111
 *
112
 *     "this phrase" special_operator:"[100 to 500]" -word -"more words" -word_1 word_2
113
 *
114
 * The search criteria above will be treated as it were composed by the
115
 * following parts:
116
 *
117
 * - `this phrase`
118
 * - `special_operator:[100 to 500]`
119
 * - `-word`
120
 * - `-more words`
121
 * - `-word_1`
122
 * - `word_2`
123
 *
124
 * ---
125
 *
126
 * Search criteria allows you to perform complex search conditions in a
127
 * human-readable way. Allows you, for example, create user-friendly search-forms,
128
 * or create some RSS feed just by creating a friendly URL using a search-criteria.
129
 * e.g.: `http://example.com/rss/category:art date:>2014-01-01`
130
 *
131
 * You must use the `search()` method to scope any query using a search-criteria.
132
 * For example, in one controller using `Users` model:
133
 *
134
 * ```php
135
 * $criteria = '"this phrase" OR -"not this one" AND this';
136
 * $query = $this->Users->find();
137
 * $query = $this->Users->search($criteria, $query);
138
 * ```
139
 *
140
 * The above will alter the given $query object according to the given criteria.
141
 * The second argument (query object) is optional, if not provided this Behavior
142
 * automatically generates a find-query for you. Previous example and the one
143
 * below are equivalent:
144
 *
145
 * ```php
146
 * $criteria = '"this phrase" OR -"not this one" AND this';
147
 * $query = $this->Users->search($criteria);
148
 * ```
149
 */
150
class GenericEngine extends BaseEngine
151
{
152
153
    /**
154
     * {@inheritDoc}
155
     *
156
     * - operators: A list of registered operators methods as `name` =>
157
     *   `methodName`.
158
     *
159
     * - strict: Used to filter any invalid word. Set to a string representing a
160
     *   regular expression describing which charaters should be removed. Or set
161
     *   to TRUE to used default discard criteria: only letters, digits and few
162
     *   basic symbols (".", ",", "/", etc). Defaults to TRUE (custom filter
163
     *   regex).
164
     *
165
     * - bannedWords: Array list of banned words, or a callable that should decide
166
     *   if the given word is banned or not. Defaults to empty array (allow
167
     *   everything).
168
     *
169
     * - fulltext: Whether to use FULLTEXT search whenever it is possible. Defaults to
170
     *   TRUE. This feature is only supported for MySQL InnoDB database engines.
171
     *
172
     * - datasetTable: Name of the MySQL table where words dataset should be stored and
173
     *   read from. This allows you to split large sets into different tables.
174
     */
175
    protected $_defaultConfig = [
176
        'operators' => [],
177
        'strict' => true,
178
        'bannedWords' => [],
179
        'fulltext' => true,
180
        'datasetTable' => 'search_datasets',
181
    ];
182
183
    /**
184
     * {@inheritDoc}
185
     *
186
     * @throws \Search\Engine\Generic\Exception\CompoundPrimaryKeyException When using
187
     *   compound primary keys
188
     */
189
    public function __construct(Table $table, array $config = [])
190
    {
191
        $config['tableAlias'] = (string)Inflector::underscore($table->table());
0 ignored issues
show
Deprecated Code introduced by
The method Cake\ORM\Table::table() has been deprecated with message: 3.4.0 Use setTable()/getTable() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
192
        $config['pk'] = $table->primaryKey();
0 ignored issues
show
Deprecated Code introduced by
The method Cake\ORM\Table::primaryKey() has been deprecated with message: 3.4.0 Use setPrimaryKey()/getPrimaryKey() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
193
        if (is_array($config['pk'])) {
194
            throw new CompoundPrimaryKeyException($config['tableAlias']);
195
        }
196
197
        parent::__construct($table, $config);
198
199
        $assocOptions = [
200
            'foreignKey' => 'entity_id',
201
            'joinType' => 'INNER',
202
            'conditions' => [
203
                'SearchDatasets.table_alias' => $config['tableAlias'],
204
            ],
205
            'dependent' => true
206
        ];
207
208
        if ($this->config('datasetTable') != $this->_defaultConfig['datasetTable']) {
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\InstanceConfigTrait::config() has been deprecated with message: 3.4.0 use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
209
            $datasetTableObject = clone TableRegistry::get('Search.SearchDatasets');
210
            $datasetTableObject->table($this->config('datasetTable'));
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\InstanceConfigTrait::config() has been deprecated with message: 3.4.0 use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
Deprecated Code introduced by
The method Cake\ORM\Table::table() has been deprecated with message: 3.4.0 Use setTable()/getTable() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
211
            $assocOptions['targetTable'] = $datasetTableObject;
212
        }
213
214
        $this->_table->hasOne('Search.SearchDatasets', $assocOptions);
215
    }
216
217
    /**
218
     * {@inheritDoc}
219
     */
220
    public function index(EntityInterface $entity)
221
    {
222
        $set = $this->_table->SearchDatasets->find()
223
            ->where([
224
                'entity_id' => $this->_entityId($entity),
0 ignored issues
show
Deprecated Code introduced by
The method Search\Engine\Generic\GenericEngine::_entityId() has been deprecated with message: Use direct access as `$entity->get($this->config('pk'))`

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
225
                'table_alias' => $this->config('tableAlias'),
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\InstanceConfigTrait::config() has been deprecated with message: 3.4.0 use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
226
            ])
227
            ->limit(1)
228
            ->first();
229
230
        if (!$set) {
231
            $set = $this->_table->SearchDatasets->newEntity([
232
                'entity_id' => $this->_entityId($entity),
0 ignored issues
show
Deprecated Code introduced by
The method Search\Engine\Generic\GenericEngine::_entityId() has been deprecated with message: Use direct access as `$entity->get($this->config('pk'))`

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
233
                'table_alias' => $this->config('tableAlias'),
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\InstanceConfigTrait::config() has been deprecated with message: 3.4.0 use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
234
                'words' => '',
235
            ]);
236
        }
237
238
        // We add starting and trailing space to allow LIKE %something-to-match%
239
        $set = $this->_table->SearchDatasets->patchEntity($set, [
240
            'words' => ' ' . $this->_extractEntityWords($entity) . ' '
241
        ]);
242
243
        return (bool)$this->_table->SearchDatasets->save($set);
244
    }
245
246
    /**
247
     * {@inheritDoc}
248
     */
249
    public function delete(EntityInterface $entity)
250
    {
251
        $this->_table->SearchDatasets->deleteAll([
252
            'entity_id' => $this->_entityId($entity),
0 ignored issues
show
Deprecated Code introduced by
The method Search\Engine\Generic\GenericEngine::_entityId() has been deprecated with message: Use direct access as `$entity->get($this->config('pk'))`

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
253
            'table_alias' => $this->config('tableAlias'),
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\InstanceConfigTrait::config() has been deprecated with message: 3.4.0 use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
254
        ]);
255
256
        return true;
257
    }
258
259
    /**
260
     * {@inheritDoc}
261
     */
262
    public function get(EntityInterface $entity)
263
    {
264
        return $this->_table->SearchDatasets->find()
265
            ->where([
266
                'entity_id' => $this->_entityId($entity),
0 ignored issues
show
Deprecated Code introduced by
The method Search\Engine\Generic\GenericEngine::_entityId() has been deprecated with message: Use direct access as `$entity->get($this->config('pk'))`

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
267
                'table_alias' => $this->config('tableAlias'),
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\InstanceConfigTrait::config() has been deprecated with message: 3.4.0 use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
268
            ])
269
            ->limit(1)
270
            ->first();
271
    }
272
273
    /**
274
     * {@inheritDoc}
275
     *
276
     * It looks for search-criteria and applies them over the query object. For
277
     * example, given the criteria below:
278
     *
279
     *     "this phrase" -"and not this one"
280
     *
281
     * Alters the query object as follow:
282
     *
283
     * ```php
284
     * $query->where([
285
     *    'indexed_words LIKE' => '%this phrase%',
286
     *    'indexed_words NOT LIKE' => '%and not this one%'
287
     * ]);
288
     * ```
289
     *
290
     * The `AND` & `OR` keywords are allowed to create complex conditions. For
291
     * example:
292
     *
293
     *     "this phrase" OR -"and not this one" AND "this"
294
     *
295
     * Will produce something like:
296
     *
297
     * ```php
298
     * $query->where(['indexed_words LIKE' => '%this phrase%'])
299
     *     ->orWhere(['indexed_words NOT LIKE' => '%and not this one%']);
300
     *     ->andWhere(['indexed_words LIKE' => '%this%']);
301
     * ```
302
     */
303
    public function search($criteria, Query $query)
304
    {
305
        $tokens = (array)(new MiniLanguageParser($criteria))->parse();
306
307
        if (!empty($tokens)) {
308
            $query->innerJoinWith('SearchDatasets');
309
310
            foreach ($tokens as $token) {
311
                if ($token->isOperator()) {
312
                    $query = $this->_scopeOperator($query, $token);
313
                } else {
314
                    $query = $this->_scopeWords($query, $token);
315
                }
316
            }
317
        }
318
319
        return $query;
320
    }
321
322
    /**
323
     * Scopes the given query using the given operator token.
324
     *
325
     * @param \Cake\ORM\Query $query The query to scope
326
     * @param \Search\Token $token Token describing an operator. e.g `-op_name:op_value`
327
     * @return \Cake\ORM\Query Scoped query
328
     */
329
    protected function _scopeOperator(Query $query, TokenInterface $token)
330
    {
331
        return $this->_table->applySearchOperator($query, $token);
332
    }
333
334
    /**
335
     * Scopes the given query using the given words token.
336
     *
337
     * @param \Cake\ORM\Query $query The query to scope
338
     * @param \Search\TokenInterface $token Token describing a words sequence. e.g `this is a phrase`
339
     * @return \Cake\ORM\Query Scoped query
340
     */
341
    protected function _scopeWords(Query $query, TokenInterface $token)
342
    {
343
        if ($this->_isFullTextEnabled()) {
344
            return $this->_scopeWordsInFulltext($query, $token);
345
        }
346
347
        $like = 'LIKE';
348
        if ($token->negated()) {
349
            $like = 'NOT LIKE';
350
        }
351
352
        // * Matches any one or more characters.
353
        // ! Matches any single character.
354
        $value = str_replace(['*', '!'], ['%', '_'], $token->value());
355
356
        if ($token->where() === 'or') {
357
            $query->orWhere(["SearchDatasets.words {$like}" => "%{$value}%"]);
358
        } elseif ($token->where() === 'and') {
359
            $query->andWhere(["SearchDatasets.words {$like}" => "%{$value}%"]);
360
        } else {
361
            $query->where(["SearchDatasets.words {$like}" => "%{$value}%"]);
362
        }
363
364
        return $query;
365
    }
366
367
    /**
368
     * Similar to "_scopeWords" but using MySQL's fulltext indexes.
369
     *
370
     * @param \Cake\ORM\Query $query The query to scope
371
     * @param \Search\TokenInterface $token Token describing a words sequence. e.g `this is a phrase`
372
     * @return \Cake\ORM\Query Scoped query
373
     */
374
    protected function _scopeWordsInFulltext(Query $query, TokenInterface $token)
375
    {
376
        $value = str_replace(['*', '!'], ['*', '*'], $token->value());
377
        $value = mb_strpos($value, '+') === 0 ? mb_substr($value, 1) : $value;
378
379
        if (empty($value) || in_array($value, $this->_stopWords())) {
380
            return $query;
381
        }
382
383
        $not = $token->negated() ? 'NOT' : '';
384
        $value = str_replace("'", '"', $value);
385
        $conditions = ["{$not} MATCH(SearchDatasets.words) AGAINST('{$value}' IN BOOLEAN MODE) > 0"];
386
387 View Code Duplication
        if ($token->where() === 'or') {
388
            $query->orWhere($conditions);
389
        } elseif ($token->where() === 'and') {
390
            $query->andWhere($conditions);
391
        } else {
392
            $query->where($conditions);
393
        }
394
395
        return $query;
396
    }
397
398
    /**
399
     * Whether FullText index is available or not and should be used.
400
     *
401
     * @return bool True if enabled and should be used, false otherwise
402
     */
403
    protected function _isFullTextEnabled()
404
    {
405
        if (!$this->config('fulltext')) {
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\InstanceConfigTrait::config() has been deprecated with message: 3.4.0 use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
406
            return false;
407
        }
408
409
        static $enabled = null;
410
        if ($enabled !== null) {
411
            return $enabled;
412
        }
413
414
        list(, $driverClass) = namespaceSplit(strtolower(get_class($this->_table->connection()->driver())));
0 ignored issues
show
Deprecated Code introduced by
The method Cake\ORM\Table::connection() has been deprecated with message: 3.4.0 Use setConnection()/getConnection() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
415
        if ($driverClass != 'mysql') {
416
            $enabled = false;
417
418
            return false;
419
        }
420
421
        $schema = $this->_table->SearchDatasets->schema();
422
        foreach ($schema->indexes() as $index) {
423
            $info = $schema->index($index);
424
            if (in_array('words', $info['columns']) &&
425
                strtolower($info['type']) == 'fulltext'
426
            ) {
427
                $enabled = true;
428
429
                return true;
430
            }
431
        }
432
433
        $enabled = false;
434
435
        return false;
436
    }
437
438
    /**
439
     * Gets a list of storage engine's stopwords. That is words that is considered
440
     * common or Trivial enough that it is omitted from the search index and ignored
441
     * in search queries
442
     *
443
     * @return array List of words
444
     */
445
    protected function _stopWords()
446
    {
447
        $conn = $this->_table->find()->connection();
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Database\Query::connection() has been deprecated with message: 3.4.0 Use setConnection()/getConnection() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
448
        $cacheKey = $conn->configName() . '_generic_engine_stopwords_list';
449
        if ($cache = Cache::read($cacheKey, '_cake_model_')) {
450
            return (array)$cache;
451
        }
452
453
        $words = [];
454
        $sql = $conn
455
            ->execute('SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD')
456
            ->fetchAll('assoc');
457
458
        foreach ((array)$sql as $row) {
459
            if (!empty($row['value'])) {
460
                $words[] = $row['value'];
461
            }
462
        }
463
464
        Cache::write($cacheKey, $words, '_cake_model_');
465
466
        return $words;
467
    }
468
469
    /**
470
     * Calculates entity's primary key.
471
     *
472
     * @param \Cake\Datasource\EntityInterface $entity The entity
473
     * @return string
474
     * @deprecated Use direct access as `$entity->get($this->config('pk'))`
475
     */
476
    protected function _entityId(EntityInterface $entity)
477
    {
478
        return $entity->get($this->config('pk'));
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\InstanceConfigTrait::config() has been deprecated with message: 3.4.0 use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
479
    }
480
481
    /**
482
     * Extracts a list of words to by indexed for given entity.
483
     *
484
     * NOTE: Words can be repeated, this allows to search phrases.
485
     *
486
     * @param \Cake\Datasource\EntityInterface $entity The entity for which generate
487
     *  the list of words
488
     * @return string Space-separated list of words. e.g. `cat dog this that`
489
     */
490
    protected function _extractEntityWords(EntityInterface $entity)
491
    {
492
        $text = '';
493
        $entityArray = $entity->toArray();
494
        $entityArray = Hash::flatten($entityArray);
495
        foreach ($entityArray as $key => $value) {
496
            if (is_string($value) || is_numeric($value)) {
497
                $text .= " {$value}";
498
            }
499
        }
500
501
        $text = str_replace(["\n", "\r"], '', trim((string)$text)); // remove new lines
502
        $text = strip_tags($text); // remove HTML tags, but keep their content
503
        $strict = $this->config('strict');
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\InstanceConfigTrait::config() has been deprecated with message: 3.4.0 use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
504
505
        if (!empty($strict)) {
506
            // only: space, digits (0-9), letters (any language), ".", ",", "-", "_", "/", "\"
507
            $pattern = is_string($strict) ? $strict : '[^\p{L}\p{N}\s\@\.\,\-\_\/\\0-9]';
508
            $text = preg_replace('/' . $pattern . '/ui', ' ', $text);
509
        }
510
511
        $text = trim(preg_replace('/\s{2,}/i', ' ', $text)); // remove double spaces
512
        $text = mb_strtolower($text); // all to lowercase
513
        $text = $this->_filterText($text); // filter
514
        $text = iconv('UTF-8', 'UTF-8//IGNORE', mb_convert_encoding($text, 'UTF-8')); // remove any invalid character
515
516
        return trim($text);
517
    }
518
519
    /**
520
     * Removes any invalid word from the given text.
521
     *
522
     * @param string $text The text to filter
523
     * @return string Filtered text
524
     */
525
    protected function _filterText($text)
526
    {
527
        // return true means `yes, it's banned`
528
        if (is_callable($this->config('bannedWords'))) {
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\InstanceConfigTrait::config() has been deprecated with message: 3.4.0 use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
529
            $isBanned = function ($word) {
530
                $callable = $this->config('bannedWords');
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Core\InstanceConfigTrait::config() has been deprecated with message: 3.4.0 use setConfig()/getConfig() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
531
532
                return $callable($word);
533
            };
534
        } else {
535
            $isBanned = function ($word) {
536
                return in_array($word, (array)$this->config('bannedWords')) || empty($word);
537
            };
538
        }
539
540
        $words = explode(' ', $text);
541
        foreach ($words as $i => $w) {
542
            if ($isBanned($w)) {
543
                unset($words[$i]);
544
            }
545
        }
546
547
        return implode(' ', $words);
548
    }
549
}
550