diff --git a/src/Plugin/Scanner/Node.php b/src/Plugin/Scanner/Node.php index 4dd0b95da7d02b0d47b345ab59a5a8051f92ef3f..b8744e9422e1892448099f7f07b136511b4c274d 100644 --- a/src/Plugin/Scanner/Node.php +++ b/src/Plugin/Scanner/Node.php @@ -40,52 +40,81 @@ class Node extends Entity { $conditionVals = parent::buildCondition($values['search'], $values['mode'], $values['wholeword'], $values['regex'], $values['preceded'], $values['followed']); $this->addQueryCondition($query, $conditionVals, $fieldname, $values['mode'], $values['language']); - // Disable the normal access check. + // Run Drupal database query. $query->accessCheck(FALSE); + $nids = $query->execute(); + + // Iterate over matched entities (nodes) in all saerched languages + // to extract information that will be rendered in the results. + $languages = $values['language'] === 'all' ? array_keys(AdminHelper::getAllEnabledLanguages()) : [$values['language']]; + foreach ($nids as $nid) { + foreach ($languages as $language) { + // Results key must be unique, accounting for > 1 language. + $result_key = $values['language'] === 'all' ? "$nid-$language" : "$nid"; + + $node = CoreNode::load($nid); + if (!$node->hasTranslation($language)) { + continue; + } + $node = $node->getTranslation($language); + $nodeField = $node->get($fieldname); + $fieldType = $nodeField->getFieldDefinition()->getType(); + + if (in_array($fieldType, ['text_with_summary', 'text', 'text_long'])) { + // @todo Add support for fields with cardinality > 1. + $fieldValue = $nodeField->getValue()[0] ?? []; + + // Find all instances of the term we're looking for. + preg_match_all($conditionVals['phpRegex'], $fieldValue['value'], $matches, PREG_OFFSET_CAPTURE); + if (empty($matches) || !isset($matches[0]) || !isset($matches[0][0])) { + // Match not guaranteed for nodes with > 1 language. + continue; + } - $entities = $query->execute(); - // Iterate over matched entities (nodes) to extract information that will - // be rendered in the results. - foreach ($entities as $id) { - $node = CoreNode::load($id); - $nodeField = $node->get($fieldname); - $fieldType = $nodeField->getFieldDefinition()->getType(); - if (in_array($fieldType, ['text_with_summary', 'text', 'text_long'])) { - $fieldValue = $nodeField->getValue()[0]; - $title_collect[$id]['title'] = $node->getTitle(); - // Find all instances of the term we're looking for. - preg_match_all($conditionVals['phpRegex'], $fieldValue['value'], $matches, PREG_OFFSET_CAPTURE); - $newValues = []; - // Build an array of strings which are displayed in the results. - foreach ($matches[0] as $v) { - // The offset of the matched term(s) in the field's text. - $start = $v[1]; - if ($values['preceded'] !== '') { - // Bolding won't work if starting position is in the middle of a - // word (non-word bounded searches), therefore move the start - // position back as many character as there are in the 'preceded' - // text. - $start -= strlen($values['preceded']); + // Build an array of strings which are displayed in the results. + $newValues = []; + foreach ($matches[0] as $v) { + // The offset of the matched term(s) in the field's text. + $start = $v[1]; + if ($values['preceded'] !== '') { + // Bolding won't work if starting position is in the middle of + // a word (non-word bounded searches), therefore move the start + // position back as many character as there are in the + // 'preceded' text. + $start -= strlen($values['preceded']); + } + // Extract part of the text which include the search term plus six + // "words" following it. After finding the string, bold the search + // term. + $replaced = preg_replace($conditionVals['phpRegex'], "<strong>$v[0]</strong>", preg_split("/\s+/", substr($fieldValue['value'], $start), 6)); + if (count($replaced) > 1) { + // The final index contains the remainder of the text, which we + // don't care about, so we discard it. + array_pop($replaced); + } + $newValues[] = implode(' ', $replaced); } - // Extract part of the text which include the search term plus six - // "words" following it. After finding the string, bold the search - // term. - $replaced = preg_replace($conditionVals['phpRegex'], "<strong>$v[0]</strong>", preg_split("/\s+/", substr($fieldValue['value'], $start), 6)); - if (count($replaced) > 1) { - // The final index contains the remainder of the text, which we - // don't care about, so we discard it. - array_pop($replaced); + + $title_collect[$result_key]['title'] = $node->getTitle(); + $title_collect[$result_key]['field'] = $newValues; + $title_collect[$result_key]['view_url'] = $node->toUrl()->toString(); + $title_collect[$result_key]['edit_url'] = $node->toUrl('edit-form')->toString(); + } + elseif (in_array($fieldType, ['string', 'link'])) { + preg_match($conditionVals['phpRegex'], $nodeField->getString(), $matches, PREG_OFFSET_CAPTURE); + if (empty($matches) || !isset($matches[0]) || !isset($matches[0][0])) { + // Match not guaranteed for nodes with > 1 language. + continue; } - $newValues[] = implode(' ', $replaced); + + $match = $matches[0][0]; + $replaced = preg_replace($conditionVals['phpRegex'], "<strong>$match</strong>", $nodeField->getString()); + + $title_collect[$result_key]['title'] = $node->getTitle(); + $title_collect[$result_key]['field'] = [$replaced]; + $title_collect[$result_key]['view_url'] = $node->toUrl()->toString(); + $title_collect[$result_key]['edit_url'] = $node->toUrl('edit-form')->toString(); } - $title_collect[$id]['field'] = $newValues; - } - elseif (in_array($fieldType, ['string', 'link'])) { - $title_collect[$id]['title'] = $node->getTitle(); - preg_match($conditionVals['phpRegex'], $nodeField->getString(), $matches, PREG_OFFSET_CAPTURE); - $match = $matches[0][0]; - $replaced = preg_replace($conditionVals['phpRegex'], "<strong>$match</strong>", $nodeField->getString()); - $title_collect[$id]['field'] = [$replaced]; } } } @@ -101,9 +130,12 @@ class Node extends Entity { */ public function replace(string $field, array $values, array $undo_data): array { $data = $undo_data; - [$entityType, $bundle, $fieldname] = explode(':', $field); try { + // $field will be string composed of entity type, bundle name, and field + // name delimited by ':' characters. + [$entityType, $bundle, $fieldname] = explode(':', $field); + $query = $this->entityTypeManager->getStorage($entityType)->getQuery(); $query->condition('type', $bundle); if ($values['published']) { @@ -115,77 +147,53 @@ class Node extends Entity { // Disable the normal access check. $query->accessCheck(FALSE); - $entities = $query->execute(); + $languages = $values['language'] === 'all' ? array_keys(AdminHelper::getAllEnabledLanguages()) : [$values['language']]; + $nids = $query->execute(); - foreach ($entities as $id) { - $node = CoreNode::load($id); - $nodeField = $node->get($fieldname); - $fieldType = $nodeField->getFieldDefinition()->getType(); - if (in_array($fieldType, ['text_with_summary', 'text', 'text_long'])) { - if ($values['language'] === 'all') { - $other_languages = AdminHelper::getAllEnabledLanguages(); - foreach ($other_languages as $langcode => $languageName) { - if ($node->hasTranslation($langcode)) { - $node = $node->getTranslation($langcode); - $nodeField = $node->get($fieldname); - } - $fieldValue = $nodeField->getValue()[0]; - // Replace the search term with the replacement term. - $fieldValue['value'] = preg_replace($conditionVals['phpRegex'], $values['replace'], $fieldValue['value']); - $node->$fieldname = $fieldValue; - } + // Iterate over matched entities (nodes) to perform the replacement. + foreach ($nids as $nid) { + foreach ($languages as $language) { + $node = CoreNode::load($nid); + if ($node->hasTranslation($language)) { + $node = $node->getTranslation($language); } - else { - $requested_lang = $values['language']; - if ($node->hasTranslation($requested_lang)) { - $node = $node->getTranslation($requested_lang); - $nodeField = $node->get($fieldname); - } - $fieldValue = $nodeField->getValue()[0]; + $nodeField = $node->get($fieldname); + $fieldType = $nodeField->getFieldDefinition()->getType(); + + if (in_array($fieldType, ['text_with_summary', 'text', 'text_long'])) { + // @todo Add support for fields with cardinality > 1. + $fieldValue = $nodeField->getValue()[0] ?? []; + // Replace the search term with the replacement term. $fieldValue['value'] = preg_replace($conditionVals['phpRegex'], $values['replace'], $fieldValue['value']); $node->$fieldname = $fieldValue; + } - if (!isset($data["node:$id"]['new_vid'])) { - $data["node:$id"]['old_vid'] = $node->vid->getString(); - // Crete a new revision so that we can have the option of undoing it - // later on. - $node->setNewRevision(); - $node->revision_log = $this->t('Replaced %search with %replace via Scanner Search and Replace module.', [ - '%search' => $values['search'], - '%replace' => $values['replace'], - ]); - $node->setRevisionUserId($this->currentUser->id()); + elseif ($fieldType == 'string') { + $fieldValue = preg_replace($conditionVals['phpRegex'], $values['replace'], $nodeField->getString()); + $node->$fieldname = $fieldValue; } - // Save the updated node. - $node->save(); - // Fetch the new revision id. - $data["node:$id"]['new_vid'] = $node->vid->getString(); - } - elseif ($fieldType == 'string') { - if (!isset($data["node:$id"]['new_vid'])) { - if ($values['language'] === 'all') { - $all_languages = AdminHelper::getAllEnabledLanguages(); - foreach ($all_languages as $langcode => $languageName) { - if ($node->hasTranslation($langcode)) { - $node = $node->getTranslation($langcode); - $nodeField = $node->get($fieldname); + elseif ($fieldType == 'link') { + $new_value = []; + foreach ($nodeField->getValue() as $delta => $field_value) { + foreach ($field_value as $field_element => $field_element_value) { + if (is_string($field_element_value)) { + $fieldValue = preg_replace($conditionVals['phpRegex'], $values['replace'], $field_element_value); } - $fieldValue = preg_replace($conditionVals['phpRegex'], $values['replace'], $nodeField->getString()); - $node->$fieldname = $fieldValue; - } - } - else { - $requested_lang = $values['language']; - if ($node->hasTranslation($requested_lang)) { - // $nodeField = $nodeField->getTranslation($requested_lang); - $node = $node->getTranslation($requested_lang); - $nodeField = $node->get($fieldname); + else { + $fieldValue = $field_element_value; + } + $new_value[$delta][$field_element] = $fieldValue; } - $fieldValue = preg_replace($conditionVals['phpRegex'], $values['replace'], $nodeField->getString()); - $node->$fieldname = $fieldValue; } - $data["node:$id"]['old_vid'] = $node->vid->getString(); + $node->$fieldname = $new_value; + } + + // Create a new revision and save the node for this translation. + if (!isset($data["node:$nid"]['new_vid'])) { + $data["node:$nid"]['old_vid'] = $node->vid->getString(); + // Crete a new revision so that we can have the option of undoing it + // later on. $node->setNewRevision(); $node->revision_log = $this->t('Replaced %search with %replace via Scanner Search and Replace module.', [ '%search' => $values['search'], @@ -193,70 +201,11 @@ class Node extends Entity { ]); $node->setRevisionUserId($this->currentUser->id()); } + + // Save the updated node. $node->save(); - $data["node:$id"]['new_vid'] = $node->vid->getString(); - } - elseif ($fieldType == 'link') { - if (!isset($data["node:$id"]['new_vid'])) { - if ($values['language'] === 'all') { - $all_languages = AdminHelper::getAllEnabledLanguages(); - foreach ($all_languages as $langcode => $languageName) { - if ($node->hasTranslation($langcode)) { - $node = $node->getTranslation($langcode); - $nodeField = $node->get($fieldname); - } - $new_value = []; - foreach ($nodeField->getValue() as $delta => $field_value) { - foreach ($field_value as $field_element => $field_element_value) { - if (is_string($field_element_value)) { - $fieldValue = preg_replace($conditionVals['phpRegex'], $values['replace'], $field_element_value); - } - else { - $fieldValue = $field_element_value; - } - $new_value[$delta][$field_element] = $fieldValue; - } - } - $node->$fieldname = $new_value; - } - $data["node:$id"]['old_vid'] = $node->vid->getString(); - $node->setNewRevision(); - $node->revision_log = $this->t('Replaced %search with %replace via Scanner Search and Replace module.', [ - '%search' => $values['search'], - '%replace' => $values['replace'], - ]); - } - else { - $requested_lang = $values['language']; - if ($node->hasTranslation($requested_lang)) { - // $nodeField = $nodeField->getTranslation($requested_lang); - $node = $node->getTranslation($requested_lang); - $nodeField = $node->get($fieldname); - } - $new_value = []; - foreach ($nodeField->getValue() as $delta => $field_value) { - foreach ($field_value as $field_element => $field_element_value) { - if (is_string($field_element_value)) { - $fieldValue = preg_replace($conditionVals['phpRegex'], $values['replace'], $field_element_value); - } - else { - $fieldValue = $field_element_value; - } - $new_value[$delta][$field_element] = $fieldValue; - } - } - $node->$fieldname = $new_value; - $data["node:$id"]['old_vid'] = $node->vid->getString(); - $node->setNewRevision(); - $node->revision_log = $this->t('Replaced %search with %replace via Scanner Search and Replace module.', [ - '%search' => $values['search'], - '%replace' => $values['replace'], - ]); - } - } - $node->setRevisionUserId($this->currentUser->id()); - $node->save(); - $data["node:$id"]['new_vid'] = $node->vid->getString(); + // Fetch the new revision id. + $data["node:$nid"]['new_vid'] = $node->vid->getString(); } } } diff --git a/src/Plugin/Scanner/Paragraph.php b/src/Plugin/Scanner/Paragraph.php index 0d29ddb436fc03672011dd2f16405ba06172f9d9..b522e71a5e7c295830b7befbb476029aba9c9932 100644 --- a/src/Plugin/Scanner/Paragraph.php +++ b/src/Plugin/Scanner/Paragraph.php @@ -7,6 +7,7 @@ use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Logger\LoggerChannelTrait; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\scanner\AdminHelper; /** * A Scanner plugin for handling Paragraph entities. @@ -27,10 +28,10 @@ class Paragraph extends Entity { public function search(string $field, array $values): array { $title_collect = []; try { - [, $bundle, $fieldname] = explode(':', $field); + [$entityType, $bundle, $fieldname] = explode(':', $field); - $query = $this->entityTypeManager->getStorage('paragraph')->getQuery(); - $query->condition('type', $bundle); + $query = $this->entityTypeManager->getStorage($entityType)->getQuery(); + $query->condition('type', $bundle, '='); if ($values['published']) { $query->condition('status', 1); } @@ -40,82 +41,99 @@ class Paragraph extends Entity { // Disable the normal access check. $query->accessCheck(FALSE); + $languages = $values['language'] === 'all' ? array_keys(AdminHelper::getAllEnabledLanguages()) : [$values['language']]; $entities = $query->execute(); + if (empty($entities)) { + return $title_collect; + } - if (!empty($entities)) { - // Load the paragraph(s) which match the criteria. - $paragraphs = $this->entityTypeManager->getStorage('paragraph') - ->loadMultiple($entities); - // Iterate over matched paragraphs to extract information that will be - // rendered in the results. - foreach ($paragraphs as $paragraph) { - if (!empty($paragraph)) { - // Load the entity the paragraph is referenced in. - $parentEntity = $paragraph->getParentEntity(); + // Load the paragraph(s) which match the criteria. + /** @var \Drupal\paragraphs\Entity\Paragraph[] $paragraphs */ + $paragraphs = $this->entityTypeManager->getStorage('paragraph')->loadMultiple($entities); + + foreach ($paragraphs as $paragraph) { + foreach ($languages as $language) { + if ($paragraph->hasTranslation($language)) { + $paragraph = $paragraph->getTranslation($language); + } - if (!empty($parentEntity)) { - $parentEntityType = $parentEntity->getEntityTypeId(); - // In the case of nested relationships we need to find the base - // entity. - if ($parentEntityType != 'node') { - // If child is only nested one level deep. - if ($parentEntity->getParentEntity()->getEntityTypeId() == 'node') { - $parentEntity = $parentEntity->getParentEntity(); - } - // Two or more levels of nesting. - else { - while ($parentEntity->getParentEntity()->getEntityTypeId() != 'node') { - $parentEntity = $parentEntity->getParentEntity(); - } - } + // Load the entity the paragraph is referenced in. + $parentEntity = $paragraph->getParentEntity(); + if (empty($parentEntity)) { + continue; + } + + // Find the base node this paragraph is associated with. + if ($parentEntity->getEntityTypeId() != 'node') { + // If child is only nested one level deep. + if ($parentEntity->getParentEntity()->getEntityTypeId() == 'node') { + $parentEntity = $parentEntity->getParentEntity(); + } + else { + // Two or more levels of nesting. + while ($parentEntity->getParentEntity()->getEntityTypeId() != 'node') { + $parentEntity = $parentEntity->getParentEntity(); } - $id = $parentEntity->id(); - // Get the value of the specified field. - $paraField = $paragraph->get($fieldname); - $fieldType = $paraField->getFieldDefinition()->getType(); - if (in_array($fieldType, [ - 'text_with_summary', - 'text', - 'text_long', - ])) { - // Get the value of the field. - $fieldValue = $paraField->getValue()[0]; - // Get the parent entity's title. - $title_collect[$id]['title'] = $parentEntity->getTitle(); - // Find all instances of the term we're looking for. - preg_match_all($conditionVals['phpRegex'], $fieldValue['value'], $matches, PREG_OFFSET_CAPTURE); - $newValues = []; - // Build an array of strings which are displayed in the results - // with the searched term bolded. - foreach ($matches[0] as $v) { - // The offset of the matched term(s) in the field's text. - $start = $v[1]; - if ($values['preceded'] !== '') { - // Bolding won't work if starting position is in the middle - // of a word (non-word bounded searches), therefore we move - // the start position back as many character as there are in - // the 'preceded' text. - $start -= strlen($values['preceded']); - } - $replaced = preg_replace($conditionVals['phpRegex'], "<strong>$v[0]</strong>", preg_split("/\s+/", substr($fieldValue['value'], $start), 6)); - if (count($replaced) > 1) { - // The final index contains the remainder of the text, which - // we don't care about, so we discard it. - array_pop($replaced); - } - $newValues[] = implode(' ', $replaced); - } - $title_collect[$id]['field'] = $newValues; + } + } + $id = $parentEntity->id(); + // Get the value of the specified field. + $paraField = $paragraph->get($fieldname); + $fieldType = $paraField->getFieldDefinition()->getType(); + if (in_array($fieldType, ['text_with_summary', 'text', 'text_long'])) { + // Get the value of the field. + // @todo Handle fields with cardinality greater than 1. + $fieldValue = $paraField->getValue()[0] ?? []; + + // Find all instances of the term we're looking for. + preg_match_all($conditionVals['phpRegex'], $fieldValue['value'], $matches, PREG_OFFSET_CAPTURE); + if (empty($matches) || !isset($matches[0]) || !isset($matches[0][0])) { + // Match not guaranteed when paragraph has > 1 language. + continue; + } + + $newValues = []; + // Build an array of strings which are displayed in the results + // with the searched term bolded. + foreach ($matches[0] as $v) { + // The offset of the matched term(s) in the field's text. + $start = $v[1]; + if ($values['preceded'] !== '') { + // Bolding won't work if starting position is in the middle + // of a word (non-word bounded searches), therefore we move + // the start position back as many character as there are in + // the 'preceded' text. + $start -= strlen($values['preceded']); } - elseif (in_array($fieldType, ['string', 'link'])) { - $title_collect[$id]['title'] = $parentEntity->getTitle(); - preg_match($conditionVals['phpRegex'], $paraField->getString(), $matches, PREG_OFFSET_CAPTURE); - $match = $matches[0][0]; - $replaced = preg_replace($conditionVals['phpRegex'], "<strong>$match</strong>", $paraField->getString()); - $title_collect[$id]['field'] = [$replaced]; + $replaced = preg_replace($conditionVals['phpRegex'], "<strong>$v[0]</strong>", preg_split("/\s+/", substr($fieldValue['value'], $start), 6)); + if (count($replaced) > 1) { + // The final index contains the remainder of the text, which + // we don't care about, so we discard it. + array_pop($replaced); } + $newValues[] = implode(' ', $replaced); } + $title_collect[$id]['title'] = $parentEntity->getTitle(); + $title_collect[$id]['field'] = $newValues; + $title_collect[$id]['view_url'] = $parentEntity->toUrl()->toString(); + $title_collect[$id]['edit_url'] = $parentEntity->toUrl('edit-form')->toString(); } + elseif (in_array($fieldType, ['string', 'link'])) { + preg_match($conditionVals['phpRegex'], $paraField->getString(), $matches, PREG_OFFSET_CAPTURE); + if (empty($matches) || !isset($matches[0]) || !isset($matches[0][0])) { + // Match not guaranteed for paragraphs with > 1 language. + continue; + } + + $match = $matches[0][0]; + $replaced = preg_replace($conditionVals['phpRegex'], "<strong>$match</strong>", $paraField->getString()); + + $title_collect[$id]['title'] = $parentEntity->getTitle(); + $title_collect[$id]['field'] = [$replaced]; + $title_collect[$id]['view_url'] = $parentEntity->toUrl()->toString(); + $title_collect[$id]['edit_url'] = $parentEntity->toUrl('edit-form')->toString(); + } + } } } diff --git a/templates/scanner-results.html.twig b/templates/scanner-results.html.twig index b1c7375b2c1f9a1da5f976751a0f613bf2ac9b47..06c762e041b76eede513bc70f37ef8be90d18b16 100644 --- a/templates/scanner-results.html.twig +++ b/templates/scanner-results.html.twig @@ -10,8 +10,8 @@ {% for id, value in values %} <li> {{value.title}} | - <a href="{{ path('entity.node.canonical', {'node':id}) }}">{{ 'view'|t }}</a> | - <a href="/node/{{id}}/edit">{{ 'edit'|t }}</a> + <a href="{{ value.view_url }}">{{ 'view'|t }}</a> | + <a href="{{ value.edit_url }}">{{ 'edit'|t }}</a> </li> {% if value.field|length > 1 %} <span>[{{ '@count matches in the field'|t({'@count': value.field|length})}}]</span>