scanner-8.x-1.0-rc3/src/Plugin/Scanner/Entity.php
src/Plugin/Scanner/Entity.php
<?php
namespace Drupal\scanner\Plugin\Scanner;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\scanner\Plugin\ScannerPluginBase;
/**
* A generic Scanner plugin for handling entities.
*
* @Scanner(
* id = "scanner_entity",
* type = "entity",
* )
*/
class Entity extends ScannerPluginBase {
use LoggerChannelTrait;
use MessengerTrait;
/**
* The scanner regular expression.
*
* @var string
*/
protected string $scannerRegexChars = '.\/+*?[^]$() {}=!<>|:';
/**
* Performs the search operation for the given string/expression.
*
* @param string $field
* The field with the matching string (formatted as type:bundle:field).
* @param array $values
* An array containing the $form_state values.
*
* @return array
* An array containing the titles of the entity and a snippet of the
* matching text.
*/
public function search(string $field, array $values): array {
$data = [];
[$entityType] = explode(':', $field);
// Attempt to load the matching plugin for the matching entity.
try {
$plugin = $this->scannerManager->createInstance("scanner_$entityType");
if (empty($plugin)) {
throw new PluginException('Unable to load entity type ' . $entityType . '.');
}
// Perform the search on the current field.
$results = $plugin->search($field, $values);
if (!empty($results)) {
$data = $results;
}
}
catch (PluginException $e) {
// The instance could not be found so fail gracefully and let the user
// know.
$this->getLogger('scanner')->error($e->getMessage());
$this->messenger()->addError($this->t('An error occurred @e:', ['@e' => $e->getMessage()]));
}
return $data;
}
/**
* Performs the replace operation for the given string/expression.
*
* @param string $field
* The field with the matching string (formatted as type:bundle:field).
* @param array $values
* An array containing the $form_state values.
* @param array $undo_data
* An array containing the data.
*
* @return array
* An array containing the revision ids of the affected entities.
*/
public function replace(string $field, array $values, array $undo_data): array {
$data = [];
[$entityType] = explode(':', $field);
try {
$plugin = $this->scannerManager->createInstance("scanner_$entityType");
// Perform the replacement on the current field and save results.
$results = $plugin->replace($field, $values, $undo_data);
if (!empty($results)) {
$data = $results;
}
}
catch (PluginException $e) {
// The instance could not be found so fail gracefully and let the user
// know.
$this->getLogger('scanner')->error($e->getMessage());
$this->messenger()->addError('An error occurred: ' . $e->getMessage());
}
return $data;
}
/**
* Undo the replace operation by reverting entities to a previous revision.
*
* @param array $data
* An array containing the revision ids needed to undo the previous replace
* operation.
*/
public function undo(array $data): void {
foreach ($data as $key => $value) {
[$entityType] = explode(':', $key);
// Attempt to load the matching plugin for the matching entity.
try {
$plugin = $this->scannerManager->createInstance("scanner_$entityType");
$plugin->undo($value);
}
catch (PluginException $e) {
$this->getLogger('scanner')->error($e->getMessage());
$this->messenger()->addError('An error occurred: ' . $e->getMessage());
}
}
}
/**
* Helper function to "build" the proper query condition.
*
* @param string $search
* The string that is to be searched for.
* @param bool $mode
* The boolean that indicated whether the search should be case-sensitive.
* @param bool $wholeword
* The boolean that indicates whether the search should be word-bounded.
* @param bool $regex
* The boolean that indicates whether the search term is a regular
* expression.
* @param string $preceded
* The string for preceded expression.
* @param string $followed
* The string for the succeeding expression.
*
* @return array
* Returns an associative array keyed by:
* - condition: The target ICU regular expression (for database use);
* - phpRegex: The target perl-compatible regular expression (for PHP);
* - operator: The operator to use in a database condition; usually one of
* 'REGEXP', 'LIKE', 'REGEXP_LIKE'.
*/
protected function buildCondition(string $search, bool $mode, bool $wholeword, bool $regex, string $preceded, string $followed): array {
$preceded_php = '';
if (!empty($preceded)) {
if (!$regex) {
$preceded = addcslashes($preceded, $this->scannerRegexChars);
}
$preceded_php = '(?<=' . $preceded . ')';
}
$followed_php = '';
if (!empty($followed)) {
if (!$regex) {
$followed = addcslashes($followed, $this->scannerRegexChars);
}
$followed_php = '(?=' . $followed . ')';
}
$word_boundaries_helper = $this->scannerHelper;
if ($word_boundaries_helper->whichToUse() === 'icu') {
$word_boundary_prefix = $word_boundary_suffix = "\\b";
}
else {
// Control which word boundaries are used, used for compatibility with
// different releases of MySQL.
// @see https://dev.mysql.com/doc/refman/8.0/en/regexp.html#regexp-compatibility
// @see https://mariadb.com/kb/en/regular-expressions-overview/#word-boundaries
$word_boundary_prefix = '[[:<:]]';
$word_boundary_suffix = '[[:>:]]';
}
// Case 1.
if ($wholeword && $regex) {
$value = $word_boundary_prefix . $preceded . $search . $followed . $word_boundary_suffix;
$operator = 'REGEXP';
$phpRegex = '/\b' . $preceded_php . $search . $followed_php . '\b/';
}
// Case 2.
elseif ($wholeword && !$regex) {
$value = $word_boundary_prefix . $preceded . addcslashes($search, $this->scannerRegexChars) . $followed . $word_boundary_suffix;
$operator = 'REGEXP';
$phpRegex = '/\b' . $preceded_php . addcslashes($search, $this->scannerRegexChars) . $followed . '\b/';
}
// Case 3.
elseif (!$wholeword && $regex) {
$value = $preceded . $search . $followed;
$operator = 'REGEXP';
$phpRegex = '/' . $preceded_php . $search . $followed_php . '/';
}
// Case 4.
else {
$value = '%' . $preceded . addcslashes($search, $this->scannerRegexChars) . $followed . '%';
$operator = 'LIKE';
$phpRegex = '/' . $preceded . addcslashes($search, $this->scannerRegexChars) . $followed . '/';
}
if ($mode) {
if ($operator === 'REGEXP' && $word_boundaries_helper->supportsInlinePcreFlags()) {
// Check if the database engine supports the inline PCRE regex. If it
// does, then we'll use that to specify the case-sensitivity rather than
// the BINARY flag.
// https://mariadb.com/kb/en/pcre/#option-setting
$value = "(?-i)$value";
}
elseif ($operator === 'REGEXP' && $word_boundaries_helper->shouldUseRegexpLike()) {
// Check if it supports the "REGEXP BINARY" syntax. If it does not, then
// we'll use REGEXP_LIKE instead.
$operator = 'REGEXP_LIKE';
}
else {
$operator .= ' BINARY';
}
}
else {
$phpRegex .= 'i';
}
return [
'condition' => $value,
'phpRegex' => $phpRegex,
'operator' => $operator,
];
}
/**
* Ensures the query is limited by the specified search conditions.
*
* @param \Drupal\Core\Entity\Query\QueryInterface $query
* The query to modify.
* @param array $condition
* An array of condition values as returned by ::buildCondition().
* @param string $fieldname
* The name of the entity field having a condition added.
* @param bool $mode
* TRUE for a case-sensitive search; FALSE otherwise.
* @param string $language
* The language code or 'all' for all languages.
*/
protected function addQueryCondition(QueryInterface $query, array $condition, string $fieldname, bool $mode, string $language): void {
if ($language !== 'all') {
$query->condition('langcode', $language, '=');
}
$langcode = $language === 'all' ? NULL : $language;
if ($condition['operator'] === 'REGEXP_LIKE') {
// The REGEXP_LIKE() function can't be added directly to the entity query,
// so we'll add a tag and add the actual condition later.
/* @see \scanner_query_scanner_search_regexp_like_alter() */
/* @see \Drupal\scanner\Plugin\Scanner\Entity::addRegexpLikeCondition() */
$query->addTag('scanner_search_regexp_like')
->addMetaData('scanner_search_regexp_like', [
'entity_type_id' => $query->getEntityTypeId(),
'fieldname' => $fieldname,
'langcode' => $langcode,
'mode' => $mode,
'pattern' => $condition['condition'],
]);
}
else {
$query->condition($fieldname, $condition['condition'], $condition['operator'], $langcode);
}
}
}
