diff --git a/core/lib/Drupal/Component/Utility/DiffArray.php b/core/lib/Drupal/Component/Utility/DiffArray.php index 719049e..b03be19 100644 --- a/core/lib/Drupal/Component/Utility/DiffArray.php +++ b/core/lib/Drupal/Component/Utility/DiffArray.php @@ -47,4 +47,35 @@ public static function diffAssocRecursive(array $array1, array $array2) { return $difference; } + /** + * Computes the difference of arrays. + * + * The main difference from the array_diff() is that this method does not + * remove duplicates. For example: + * @code + * array_diff([1, 1, 1], [1]); // [] + * \Drupal\Component\Utility\DiffArray::diffOnce([1, 1, 1], [1]); // [1, 1] + * @endcode + * + * Keys are maintained from the $array1. + * + * @param array $array1 + * The array to compare from. + * @param array $array2 + * The array to compare to. + * @param bool $strict + * The comparison mode. + * + * @return array + */ + public static function diffOnce(array $array1, array $array2, $strict = FALSE) { + foreach ($array2 as $array) { + $pos = array_search($array, $array1, $strict); + if ($pos !== FALSE) { + unset($array1[$pos]); + } + } + return $array1; + } + } diff --git a/core/modules/editor/editor.install b/core/modules/editor/editor.install index a07bd80..314bea4 100644 --- a/core/modules/editor/editor.install +++ b/core/modules/editor/editor.install @@ -20,3 +20,68 @@ function editor_update_8001() { } } } + +/** + * Recalculates file usages. + */ +function editor_update_8002(&$sandbox) { + $entity_load_limit = 50; + + if (!\Drupal::moduleHandler()->moduleExists('file')) { + return; + } + + if (!isset($sandbox['current'])) { + $file_ids = \Drupal::entityQuery('file')->execute(); + if (empty($file_ids)) { + return; + } + $sandbox['data']['file_ids'] = $file_ids; + + foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type) { + if ($entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) { + $entity_type_id = $entity_type->id(); + $entity_ids = \Drupal::entityQuery($entity_type_id)->execute(); + if (!empty($entity_ids)) { + $sandbox['data']['entity_ids'][$entity_type_id] = $entity_ids; + } + } + } + + $sandbox['current'] = 0; + $sandbox['max'] = count($sandbox['data']['file_ids']) + array_sum(array_map('count', $sandbox['data']['entity_ids'])); + } + + if (!empty($sandbox['data']['file_ids'])) { + + // Step 1: delete existing file usages. + /** @var \Drupal\file\FileUsage\FileUsageInterface $file_usage */ + $file_usage = \Drupal::service('file.usage'); + $files = \Drupal::entityTypeManager() + ->getStorage('file') + ->loadMultiple(array_splice($sandbox['data']['file_ids'], 0, $entity_load_limit)); + foreach ($files as $file) { + $usages = $file_usage->listUsage($file); + if (!empty($usages['editor'])) { + $file_usage->delete($file, 'editor', NULL, NULL, 0); + } + } + } + else { + + // Step 2: recalculate file usages. + reset($sandbox['data']['entity_ids']); + $entity_type_id = key($sandbox['data']['entity_ids']); + $entities = \Drupal::entityTypeManager() + ->getStorage($entity_type_id) + ->loadMultiple(array_splice($sandbox['data']['entity_ids'][$entity_type_id], 0, $entity_load_limit)); + foreach ($entities as $entity) { + editor_entity_insert($entity); + } + $sandbox['data']['entity_ids'] = array_filter($sandbox['data']['entity_ids']); + } + + $current_amount = count($sandbox['data']['file_ids']) + array_sum(array_map('count', $sandbox['data']['entity_ids'])); + $sandbox['current'] = $sandbox['max'] - $current_amount; + $sandbox['#finished'] = empty($current_amount) ? 1 : ($sandbox['current'] / $sandbox['max']); +} diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module index 7c822ed..3bdf2cb 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -5,7 +5,10 @@ * Adds bindings for client-side "text editors" to text formats. */ +use Drupal\Component\Utility\DiffArray; use Drupal\Component\Utility\Html; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\TypedData\TranslatableInterface; use Drupal\editor\Entity\Editor; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; @@ -369,13 +372,13 @@ function editor_entity_update(EntityInterface $entity) { // Detect file usages that should be incremented. foreach ($uuids_by_field as $field => $uuids) { - $added_files = array_diff($uuids_by_field[$field], $original_uuids_by_field[$field]); + $added_files = DiffArray::diffOnce($uuids_by_field[$field], $original_uuids_by_field[$field]); _editor_record_file_usage($added_files, $entity); } // Detect file usages that should be decremented. foreach ($original_uuids_by_field as $field => $uuids) { - $removed_files = array_diff($original_uuids_by_field[$field], $uuids_by_field[$field]); + $removed_files = DiffArray::diffOnce($original_uuids_by_field[$field], $uuids_by_field[$field]); _editor_delete_file_usage($removed_files, $entity, 1); } } @@ -456,21 +459,38 @@ function _editor_delete_file_usage(array $uuids, EntityInterface $entity, $count /** * Finds all files referenced (data-entity-uuid) by formatted text fields. * - * @param EntityInterface $entity + * @param FieldableEntityInterface $entity * An entity whose fields to analyze. * * @return array * An array of file entity UUIDs. */ -function _editor_get_file_uuids_by_field(EntityInterface $entity) { +function _editor_get_file_uuids_by_field(FieldableEntityInterface $entity) { $uuids = array(); + $field_definitions = $entity->getFieldDefinitions(); $formatted_text_fields = _editor_get_formatted_text_fields($entity); foreach ($formatted_text_fields as $formatted_text_field) { + + // In case of a translatable field, iterate over all its translations. + if ($field_definitions[$formatted_text_field]->isTranslatable() && $entity instanceof TranslatableInterface) { + $langcodes = array_keys($entity->getTranslationLanguages()); + } + else { + $langcodes = [LanguageInterface::LANGCODE_NOT_APPLICABLE]; + } + $text = ''; - $field_items = $entity->get($formatted_text_field); - foreach ($field_items as $field_item) { - $text .= $field_item->value; + foreach ($langcodes as $langcode) { + if ($langcode == LanguageInterface::LANGCODE_NOT_APPLICABLE) { + $field_items = $entity->get($formatted_text_field); + } + else { + $field_items = $entity->getTranslation($langcode)->get($formatted_text_field); + } + foreach ($field_items as $field_item) { + $text .= $field_item->value; + } } $uuids[$formatted_text_field] = _editor_parse_file_uuids($text); } diff --git a/core/modules/editor/tests/src/Kernel/EditorFileUsageTest.php b/core/modules/editor/tests/src/Kernel/EditorFileUsageTest.php index a8379d1..f0e9ca5 100644 --- a/core/modules/editor/tests/src/Kernel/EditorFileUsageTest.php +++ b/core/modules/editor/tests/src/Kernel/EditorFileUsageTest.php @@ -4,6 +4,7 @@ use Drupal\editor\Entity\Editor; use Drupal\KernelTests\Core\Entity\EntityKernelTestBase; +use Drupal\language\Entity\ConfigurableLanguage; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\file\Entity\File; @@ -23,7 +24,14 @@ class EditorFileUsageTest extends EntityKernelTestBase { * * @var array */ - public static $modules = array('editor', 'editor_test', 'node', 'file'); + public static $modules = array('editor', 'editor_test', 'node', 'file', 'language'); + + /** + * Languages to enable. + * + * @var array + */ + protected $langcodes = array('fr', 'en'); protected function setUp() { parent::setUp(); @@ -41,8 +49,14 @@ protected function setUp() { )); $filtered_html_format->save(); + // Add languages. + foreach ($this->langcodes as $langcode) { + ConfigurableLanguage::createFromLangcode($langcode)->save(); + } + // Set cardinality for body field. FieldStorageConfig::loadByName('node', 'body') + ->setTranslatable(TRUE) ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) ->save(); @@ -69,6 +83,9 @@ public function testEditorEntityHooks() { 2 => 'core/misc/help.png', ); + /** @var \Drupal\file\FileUsage\FileUsageInterface $file_usage */ + $file_usage = $this->container->get('file.usage'); + $image_entities = array(); foreach ($image_paths as $key => $image_path) { $image = File::create(); @@ -76,7 +93,6 @@ public function testEditorEntityHooks() { $image->setFilename(drupal_basename($image->getFileUri())); $image->save(); - $file_usage = $this->container->get('file.usage'); $this->assertIdentical(array(), $file_usage->listUsage($image), 'The image ' . $image_paths[$key] . ' has zero usages.'); $image_entities[] = $image; @@ -104,6 +120,7 @@ public function testEditorEntityHooks() { // Test editor_entity_insert(): increment. $this->createUser(); $node = $node = Node::create([ + 'language' => 'en', 'type' => 'page', 'title' => 'test', 'body' => $body, @@ -177,11 +194,68 @@ public function testEditorEntityHooks() { $this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 2 usages.'); } + // Test editor_entity_translation_insert(): increment, by adding a new + // translation. + $translation = $node->addTranslation('fr', ['title' => 'test fr', 'body' => $original_values]); + $translation->save(); + foreach ($image_entities as $key => $image_entity) { + $this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 3 usages.'); + } + + // Test editor_entity_translation_delete(): decrement, by deleting a + // translation. + $node->removeTranslation('fr'); + $node->save(); + foreach ($image_entities as $key => $image_entity) { + $this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has 2 usages.'); + } + // Test editor_entity_delete(). $node->delete(); foreach ($image_entities as $key => $image_entity) { $this->assertIdentical(array(), $file_usage->listUsage($image_entity), 'The image ' . $image_paths[$key] . ' has zero usages again.'); } + + // Prepare a new node having only one file entity referenced in the body + // field. + $image_entity_1 = $image_entities[0]; + $body_value = ''; + $node = Node::create([ + 'language' => 'en', + 'type' => 'page', + 'title' => 'test', + 'body' => [ + 0 => [ + 'value' => $body_value, + 'format' => 'filtered_html', + ] + ], + 'uid' => 1, + ]); + $node->save(); + $nid = $node->id(); + $this->assertIdentical(array('editor' => array('node' => array($nid => '1'))), $file_usage->listUsage($image_entity_1), 'The image ' . $image_entity_1->getFileUri() . ' has 1 usage.'); + + // Add a translation. + $translation = $node->addTranslation('fr', [ + 'title' => 'test fr', + 'body' => $node->get('body')->getValue(), + ]); + $node->save(); + $this->assertIdentical(array('editor' => array('node' => array($nid => '2'))), $file_usage->listUsage($image_entity_1), 'The image ' . $image_entity_1->getFileUri() . ' has 2 usages.'); + + // Replace image in the French translation. + $image_entity_2 = $image_entities[1]; + $translation->get('body')->value = '';; + $node->save(); + $this->assertIdentical(array('editor' => array('node' => array($nid => '1'))), $file_usage->listUsage($image_entity_1), 'The image ' . $image_entity_1->getFileUri() . ' has 1 usage.'); + $this->assertIdentical(array('editor' => array('node' => array($nid => '1'))), $file_usage->listUsage($image_entity_2), 'The image ' . $image_entity_2->getFileUri() . ' has 1 usage.'); + + // Re-save translation with no image and check if usage changed + $translation->get('body')->value = ''; + $node->save(); + $this->assertIdentical(array('editor' => array('node' => array($nid => '1'))), $file_usage->listUsage($image_entity_1), 'The image ' . $image_entity_1->getFileUri() . ' has 1 usage.'); + $this->assertIdentical(array(), $file_usage->listUsage($image_entity_2), 'The image ' . $image_entity_2->getFileUri() . ' has zero usages.'); } }