diff --git a/core/lib/Drupal/Component/Utility/DiffArray.php b/core/lib/Drupal/Component/Utility/DiffArray.php
index 719049e..983c3f3 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..8596d6c 100644
--- a/core/modules/editor/editor.install
+++ b/core/modules/editor/editor.install
@@ -20,3 +20,26 @@ function editor_update_8001() {
}
}
}
+
+/**
+ * Recalculates file usages.
+ */
+function editor_update_8002() {
+ /** @var \Drupal\file\FileUsage\FileUsageInterface $file_usage */
+ $file_usage = \Drupal::service('file.usage');
+ $file_usage->clear('editor');
+
+ foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type) {
+ if ($entity_type->isSubclassOf('\Drupal\Core\Entity\FieldableEntityInterface')) {
+ $entity_type_id = $entity_type->id();
+ // Load 10 entities at once to avoid memory usage issues.
+ $result = \Drupal::entityQuery($entity_type_id)->execute();
+ while ($ids = array_splice($result, 0, 10)) {
+ $entities = \Drupal::entityTypeManager()->getStorage($entity_type_id)->loadMultiple($ids);
+ foreach ($entities as $entity) {
+ editor_entity_insert($entity);
+ }
+ }
+ }
+ }
+}
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.');
}
}
diff --git a/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php b/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php
index 12647b1..adfe6a5 100644
--- a/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php
+++ b/core/modules/file/src/FileUsage/DatabaseFileUsageBackend.php
@@ -108,4 +108,13 @@ public function listUsage(FileInterface $file) {
return $references;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function clear($module) {
+ $this->connection->delete($this->tableName)
+ ->condition('module', $module)
+ ->execute();
+ }
+
}
diff --git a/core/modules/file/src/FileUsage/FileUsageBase.php b/core/modules/file/src/FileUsage/FileUsageBase.php
index c90359b..8a8931f 100644
--- a/core/modules/file/src/FileUsage/FileUsageBase.php
+++ b/core/modules/file/src/FileUsage/FileUsageBase.php
@@ -33,4 +33,11 @@ public function delete(FileInterface $file, $module, $type = NULL, $id = NULL, $
}
}
+ /**
+ * {@inheritdoc}
+ */
+ public function clear($module) {
+ // Base implementation exists only for backward compatibility purposes.
+ }
+
}
diff --git a/core/modules/file/src/FileUsage/FileUsageInterface.php b/core/modules/file/src/FileUsage/FileUsageInterface.php
index 6acda42..6a2a76e 100644
--- a/core/modules/file/src/FileUsage/FileUsageInterface.php
+++ b/core/modules/file/src/FileUsage/FileUsageInterface.php
@@ -67,4 +67,15 @@ public function delete(FileInterface $file, $module, $type = NULL, $id = NULL, $
*/
public function listUsage(FileInterface $file);
+ /**
+ * Clear usages by module.
+ *
+ * @param string $module
+ * The name of the module.
+ *
+ * @internal This method should be used only in special cases. For example,
+ * if a module needs to completely recalculate its file usages.
+ */
+ public function clear($module);
+
}