diff --git a/core/lib/Drupal/Component/Utility/DiffArray.php b/core/lib/Drupal/Component/Utility/DiffArray.php
index 719049e305..d6a0cd64aa 100644
--- a/core/lib/Drupal/Component/Utility/DiffArray.php
+++ b/core/lib/Drupal/Component/Utility/DiffArray.php
@@ -47,4 +47,38 @@ 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.
+ *
+ * The comparison of items is always performed in the strict (===) mode.
+ *
+ * @param array $array1
+ * The array to compare from.
+ * @param array $array2
+ * The array to compare to.
+ *
+ * @return array
+ * Returns the difference between the two arrays.
+ */
+ public static function diffOnce(array $array1, array $array2) {
+ foreach ($array2 as $item) {
+ // Always use strict mode because otherwise there could be fatal errors on
+ // object conversions.
+ $key = array_search($item, $array1, TRUE);
+ if ($key !== FALSE) {
+ unset($array1[$key]);
+ }
+ }
+ return $array1;
+ }
+
}
diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module
index 8df1f0460a..cbcbc13c0b 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;
@@ -384,13 +387,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);
}
}
@@ -543,23 +546,39 @@ function editor_file_download($uri) {
/**
* Finds all files referenced (data-entity-uuid) by formatted text fields.
*
- * @param EntityInterface $entity
+ * @param \Drupal\Core\Entity\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;
- if ($field_item->getFieldDefinition()->getType() == 'text_with_summary') {
- $text .= $field_item->summary;
+ 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) {
+ if ($field_item->getFieldDefinition()->getType() == 'text_with_summary') {
+ $text .= $field_item->summary;
+ }
}
}
$uuids[$formatted_text_field] = _editor_parse_file_uuids($text);
diff --git a/core/modules/editor/editor.post_update.php b/core/modules/editor/editor.post_update.php
index 3715b6dc57..e2deb8d05e 100644
--- a/core/modules/editor/editor.post_update.php
+++ b/core/modules/editor/editor.post_update.php
@@ -5,6 +5,8 @@
* Post update functions for Editor.
*/
+use Drupal\Core\Entity\FieldableEntityInterface;
+
/**
* @addtogroup updates-8.2.x
* @{
@@ -19,3 +21,112 @@ function editor_post_update_clear_cache_for_file_reference_filter() {
/**
* @} End of "addtogroup updates-8.2.x".
*/
+
+/**
+ * @addtogroup updates-8.3.x
+ * @{
+ */
+
+/**
+ * Recalculates file usages.
+ */
+function editor_post_update_recalculate_file_usage(&$sandbox) {
+ $entity_load_limit = 50;
+
+ if (!\Drupal::moduleHandler()->moduleExists('file')) {
+ return;
+ }
+
+ if (!isset($sandbox['current'])) {
+ $result = \Drupal::entityQuery('file')->count()->execute();
+ if (empty($result)) {
+ return;
+ }
+ $sandbox['data']['total_file'] = $result;
+ $sandbox['data']['current_file_id'] = 0;
+ $sandbox['data']['count_file_id'] = 0;
+
+ foreach (\Drupal::entityTypeManager()->getDefinitions() as $entity_type) {
+ if ($entity_type->isSubclassOf(FieldableEntityInterface::class)) {
+ $entity_type_id = $entity_type->id();
+ $query = \Drupal::entityQuery($entity_type_id);
+ if ($entity_type->isRevisionable()) {
+ $query->allRevisions();
+ }
+ $result = $query->count()->execute();
+ if (!empty($result)) {
+ // We need result keys because they are either revision IDs (in case
+ // of revisionable entities) or entity IDs (in other case).
+ $sandbox['data']['total_entity_id'][$entity_type_id] = $result;
+ $sandbox['data']['current_entity_id'][$entity_type_id] = 0;
+ $sandbox['data']['count_entity_id'][$entity_type_id] = 0;
+ }
+ }
+ }
+
+ $sandbox['current'] = 0;
+ $sandbox['max'] = $sandbox['data']['total_file'] + array_sum($sandbox['data']['total_entity_id']);
+ }
+
+ if (!empty($sandbox['data']['total_file'])) {
+ // Step 1: delete existing file usages.
+ /** @var \Drupal\file\FileUsage\FileUsageInterface $file_usage */
+ $file_usage = \Drupal::service('file.usage');
+
+ $file_ids = \Drupal::entityQuery('file')
+ ->condition('fid', $sandbox['current_file_id'], '>')
+ ->sort('fid', 'ASC')
+ ->pager($entity_load_limit)
+ ->execute();
+ $files = $files = \Drupal::entityTypeManager()
+ ->getStorage('file')
+ ->loadMultiple($file_ids);
+ foreach ($files as $file) {
+ $usages = $file_usage->listUsage($file);
+ if (!empty($usages['editor'])) {
+ $file_usage->delete($file, 'editor', NULL, NULL, 0);
+ }
+ }
+
+ $sandbox['data']['count_file_id'] += count($file_ids);
+ }
+ else {
+ // Step 2: recalculate file usages.
+ reset($sandbox['data']['last_entity_id']);
+ $entity_type_id = key($sandbox['data']['last_entity_id']);
+
+ $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
+ $ids = \Drupal::entityQuery($entity_type_id)
+ ->condition($entity_type->getKey('id'), $sandbox['data']['last_entity_id'][$entity_type_id], '>')
+ ->pager($entity_load_limit)
+ ->sort($entity_type->getKey('id'), 'ASC')
+ ->execute();
+ if ($entity_type->isRevisionable()) {
+ foreach ($ids as $revision_id) {
+ $entity = \Drupal::entityTypeManager()
+ ->getStorage($entity_type_id)
+ ->loadRevision($revision_id);
+ editor_entity_insert($entity);
+ }
+ }
+ else {
+ $entities = \Drupal::entityTypeManager()
+ ->getStorage($entity_type_id)
+ ->loadMultiple($ids);
+ foreach ($entities as $entity) {
+ editor_entity_insert($entity);
+ }
+ }
+ if (empty($ids)) {
+ unset($sandbox['data']['last_entity_id'][$entity_type_id]);
+ }
+ else {
+ $sandbox['data']['count_entity_id'][$entity_type_id] += count($ids);
+ $sandbox['data']['last_entity_id'][$entity_type_id] = end($ids);
+ }
+ }
+
+ $current_amount = count($sandbox['data']['count_file_id']) + array_sum($sandbox['data']['count_entity_id']);
+ $sandbox['current'] = $sandbox['max'] - $current_amount;
+ $sandbox['#finished'] = empty($current_amount) ? 1 : ($sandbox['current'] / $sandbox['max']);
+}
diff --git a/core/modules/editor/src/Tests/Update/EditorUpdateTest.php b/core/modules/editor/src/Tests/Update/EditorUpdateTest.php
index e7f0f4435b..65cc6b72dc 100644
--- a/core/modules/editor/src/Tests/Update/EditorUpdateTest.php
+++ b/core/modules/editor/src/Tests/Update/EditorUpdateTest.php
@@ -2,6 +2,10 @@
namespace Drupal\editor\Tests\Update;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\file\Entity\File;
+use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\node\Entity\Node;
use Drupal\system\Tests\Update\UpdatePathTestBase;
/**
@@ -19,6 +23,7 @@ public function setDatabaseDumpFiles() {
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
// Simulate an un-synchronized environment.
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.editor-editor_update_8001.php',
+ __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.language-enabled.php',
];
}
@@ -67,4 +72,77 @@ public function testEditorUpdate8001() {
$this->assertIdentical($format_full_html->get('status'), $editor_full_html->get('status'));
}
+ /**
+ * Tests editor_update_8002().
+ *
+ * @see editor_update_8002()
+ */
+ public function testEditorUpdate8002() {
+
+ // Prepare second language.
+ ConfigurableLanguage::createFromLangcode('fr')->save();
+
+ // Make body field translatable.
+ FieldStorageConfig::loadByName('node', 'body')
+ ->setTranslatable(TRUE)
+ ->save();
+
+ // Prepare a test file.
+ $file_path = 'core/misc/druplicon.png';
+ $file = File::create();
+ $file->setFileUri($file_path);
+ $file->setFilename($this->container->get('file_system')->basename($file->getFileUri()));
+ $file->save();
+
+ $img_string = '';
+
+ // Initially reference file 2 times. Total file usages should be 2.
+ $node = Node::create([
+ 'language' => 'en',
+ 'type' => 'page',
+ 'title' => 'test',
+ 'body' => [
+ 0 => [
+ 'value' => str_repeat($img_string, 2),
+ 'format' => 'full_html',
+ ],
+ ],
+ 'uid' => 1,
+ ]);
+ $node->save();
+
+ // Add 2 more usages. Total file usages should be 4 now.
+ $node->get('body')->value = $node->get('body')->value . str_repeat($img_string, 2);
+ $node->save();
+
+ // Create a node translation having 5 file usages and force a new revision.
+ // Total file usages should be 15 now:
+ // initial revision: en - 4, fr - does not exists,
+ // new revision: en - 4, fr - 7.
+ $node->addTranslation('fr', [
+ 'title' => 'test fr',
+ 'body' => [
+ 0 => [
+ 'value' => str_repeat($img_string, 7),
+ 'format' => 'full_html',
+ ],
+ ],
+ ]);
+ $node->setNewRevision(TRUE);
+ $node->save();
+
+ // Now mess up file usages.
+ /** @var \Drupal\file\FileUsage\FileUsageInterface $file_usage */
+ $file_usage = $this->container->get('file.usage');
+ $file_usage->add($file, 'editor', 'node', $node->id(), 99999);
+
+ $usages = $file_usage->listUsage($file);
+ $this->assert($usages['editor']['node'][$node->id()] != 15, 'File usages are messed up.');
+
+ $this->runUpdates();
+
+ $usages = $file_usage->listUsage($file);
+ $this->assert($usages['editor']['node'][$node->id()] == 15, 'File usages are correct.');
+ }
+
}
diff --git a/core/modules/editor/tests/src/Kernel/EditorFileUsageTest.php b/core/modules/editor/tests/src/Kernel/EditorFileUsageTest.php
index 04113880cd..eea3a857aa 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;
@@ -103,7 +119,8 @@ public function testEditorEntityHooks() {
// Test editor_entity_insert(): increment.
$this->createUser();
- $node = $node = Node::create([
+ $node = Node::create([
+ 'language' => 'en',
'type' => 'page',
'title' => 'test',
'body' => $body,
@@ -199,11 +216,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/tests/Drupal/Tests/Core/Common/DiffArrayTest.php b/core/tests/Drupal/Tests/Core/Common/DiffArrayTest.php
index 4112eaeb16..3f590c8e62 100644
--- a/core/tests/Drupal/Tests/Core/Common/DiffArrayTest.php
+++ b/core/tests/Drupal/Tests/Core/Common/DiffArrayTest.php
@@ -9,27 +9,15 @@
* Tests the DiffArray helper class.
*
* @group Common
+ * @coversDefaultClass \Drupal\Component\Utility\DiffArray
*/
class DiffArrayTest extends UnitTestCase {
/**
- * Array to use for testing.
- *
- * @var array
+ * @covers ::diffAssocRecursive
*/
- protected $array1;
-
- /**
- * Array to use for testing.
- *
- * @var array
- */
- protected $array2;
-
- protected function setUp() {
- parent::setUp();
-
- $this->array1 = array(
+ public function testDiffAssocRecursive() {
+ $array1 = array(
'same' => 'yes',
'different' => 'no',
'array_empty_diff' => array(),
@@ -40,7 +28,7 @@ protected function setUp() {
'string_compared_to_array' => 'value',
'new' => 'new',
);
- $this->array2 = array(
+ $array2 = array(
'same' => 'yes',
'different' => 'yes',
'array_empty_diff' => array(),
@@ -50,12 +38,7 @@ protected function setUp() {
'array_compared_to_string' => 'value',
'string_compared_to_array' => array('value'),
);
- }
- /**
- * Tests DiffArray::diffAssocRecursive().
- */
- public function testDiffAssocRecursive() {
$expected = array(
'different' => 'no',
'int_diff' => 1,
@@ -66,7 +49,51 @@ public function testDiffAssocRecursive() {
'new' => 'new',
);
- $this->assertSame(DiffArray::diffAssocRecursive($this->array1, $this->array2), $expected);
+ $this->assertSame(DiffArray::diffAssocRecursive($array1, $array2), $expected);
+ }
+
+ /**
+ * @covers ::diffOnce
+ * @dataProvider providerTestDiffOnce
+ */
+ public function testDiffOnce($array1, $array2, $expected) {
+ $this->assertSame($expected, DiffArray::diffOnce($array1, $array2));
+ }
+
+ /**
+ * Provides test data for testDiffOnce().
+ */
+ public function providerTestDiffOnce() {
+ $object1 = new \stdClass();
+ $object2 = new \stdClass();
+ $data = [
+ [
+ [1, 1, 1, 1, 1],
+ [1, 1, 1],
+ [3 => 1, 4 => 1],
+ ],
+ [
+ [1, 1, 2, 2, 3, 3, 4],
+ ['4', 3, 2, 1],
+ [1 => 1, 3 => 2, 5 => 3, 6 => 4],
+ ],
+ [
+ [' ', '', 0, FALSE],
+ [0, 0, 0, 0, 'not used'],
+ [0 => ' ', 1 => '', 3 => FALSE],
+ ],
+ [
+ [$this, [], $object1, $object1, $object2],
+ [$object1, $object2, [], $this],
+ [3 => $object1],
+ ],
+ [
+ [['x' => ['y' => 'z']], [$object1]],
+ [1, 2, 3, '4', TRUE, [$object2], ['x' => ['y' => 'z']]],
+ [1 => [$object1]],
+ ],
+ ];
+ return $data;
}
}