Commit 338bcf9f authored by catch's avatar catch
Browse files

Issue #3056543 by plach, jungle, Berdir, catch, xjm, amateescu:...

Issue #3056543 by plach, jungle, Berdir, catch, xjm, amateescu: taxonomy_post_update_make_taxonomy_term_revisionable() and the menu link content equivalent fail when entities have no default translation
parent bc042bdc
Loading
Loading
Loading
Loading
+111 −1
Original line number Diff line number Diff line
@@ -6,12 +6,23 @@
 */

use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\menu_link_content\MenuLinkContentStorage;

/**
 * Update custom menu links to be revisionable.
 */
function menu_link_content_post_update_make_menu_link_content_revisionable(&$sandbox) {
  $finished = _menu_link_content_post_update_make_menu_link_content_revisionable__fix_default_langcode($sandbox);
  if (!$finished) {
    $sandbox['#finished'] = 0;
    return NULL;
  }

  $definition_update_manager = \Drupal::entityDefinitionUpdateManager();
  /** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository */
  $last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository');
@@ -99,5 +110,104 @@ function menu_link_content_post_update_make_menu_link_content_revisionable(&$san

  $definition_update_manager->updateFieldableEntityType($entity_type, $field_storage_definitions, $sandbox);

  if (!empty($sandbox['data_fix']['default_langcode']['processed'])) {
    $count = $sandbox['data_fix']['default_langcode']['processed'];
    if (\Drupal::moduleHandler()->moduleExists('dblog')) {
      // @todo Simplify with https://www.drupal.org/node/2548095
      $base_url = str_replace('/update.php', '', \Drupal::request()->getBaseUrl());
      $args = [
        ':url' => Url::fromRoute('dblog.overview', [], ['query' => ['type' => ['update'], 'severity' => [RfcLogLevel::WARNING]]])
          ->setOption('base_url', $base_url)
          ->toString(TRUE)
          ->getGeneratedUrl(),
      ];
      return new PluralTranslatableMarkup($count, 'Custom menu links have been converted to be revisionable. One menu link with data integrity issues was restored. More details have been <a href=":url">logged</a>.', 'Custom menu links have been converted to be revisionable. @count menu links with data integrity issues were restored. More details have been <a href=":url">logged</a>.', $args);
    }
    else {
      return new PluralTranslatableMarkup($count, 'Custom menu links have been converted to be revisionable. One menu link with data integrity issues was restored. More details have been logged.', 'Custom menu links have been converted to be revisionable. @count menu links with data integrity issues were restored. More details have been logged.');
    }
  }
  else {
    return t('Custom menu links have been converted to be revisionable.');
  }
}

/**
 * Fixes recoverable data integrity issues in the "default_langcode" field.
 *
 * @param array $sandbox
 *   The update sandbox array.
 *
 * @return bool
 *   TRUE if the operation was finished, FALSE otherwise.
 *
 * @internal
 */
function _menu_link_content_post_update_make_menu_link_content_revisionable__fix_default_langcode(array &$sandbox) {
  if (!empty($sandbox['data_fix']['default_langcode']['finished'])) {
    return TRUE;
  }

  $storage = \Drupal::entityTypeManager()->getStorage('menu_link_content');
  if (!$storage instanceof MenuLinkContentStorage) {
    return TRUE;
  }
  elseif (!isset($sandbox['data_fix']['default_langcode']['last_id'])) {
    $sandbox['data_fix']['default_langcode'] = [
      'last_id' => 0,
      'processed' => 0,
    ];
  }

  $database = \Drupal::database();
  $data_table_name = 'menu_link_content_data';
  $last_id = $sandbox['data_fix']['default_langcode']['last_id'];
  $limit = Settings::get('update_sql_batch_size', 200);

  // Detect records in the data table matching the base table language, but
  // having the "default_langcode" flag set to with 0, which is not supported.
  $query = $database->select($data_table_name, 'd');
  $query->leftJoin('menu_link_content', 'b', 'd.id = b.id AND d.langcode = b.langcode AND d.default_langcode = 0');
  $result = $query->fields('d', ['id', 'langcode'])
    ->condition('d.id', $last_id, '>')
    ->isNotNull('b.id')
    ->orderBy('d.id')
    ->range(0, $limit)
    ->execute();

  foreach ($result as $record) {
    $sandbox['data_fix']['default_langcode']['last_id'] = $record->id;

    // We need to exclude any menu link already having also a data table record
    // with the "default_langcode" flag set to 1, because this is a data
    // integrity issue that cannot be fixed automatically. However the latter
    // will not make the update fail.
    $has_default_langcode = (bool) $database->select($data_table_name, 'd')
      ->fields('d', ['id'])
      ->condition('d.id', $record->id)
      ->condition('d.default_langcode', 1)
      ->range(0, 1)
      ->execute()
      ->fetchField();

    if ($has_default_langcode) {
      continue;
    }

    $database->update($data_table_name)
      ->fields(['default_langcode' => 1])
      ->condition('id', $record->id)
      ->condition('langcode', $record->langcode)
      ->execute();

    $sandbox['data_fix']['default_langcode']['processed']++;

    \Drupal::logger('update')
      ->warning('The menu link with ID @id had data integrity issues and was restored.', ['@id' => $record->id]);
  }

  $finished = $sandbox['data_fix']['default_langcode']['last_id'] === $last_id;
  $sandbox['data_fix']['default_langcode']['finished'] = $finished;

  return $finished;
}
+114 −0
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Contains database additions to drupal-8.filled.standard.php.gz for testing
 * the upgrade path of https://www.drupal.org/project/drupal/issues/3056543.
 */

use Drupal\Core\Database\Database;

$connection = Database::getConnection();

$connection->insert('menu_link_content')
  ->fields([
    'id' => 997,
    'bundle' => 'menu_link_content',
    'uuid' => 'ea32f399-b53b-416c-81a9-e66204236c97',
    'langcode' => 'en',
  ])
  ->execute();
$connection->insert('menu_link_content_data')
  ->fields([
    'id' => 997,
    'bundle' => 'menu_link_content',
    'langcode' => 'en',
    'enabled' => 1,
    'title' => 'menu_link_997',
    'menu_name' => 'test-menu',
    'link__uri' => 'https://drupal.org',
    'link__title' => '',
    'link__options' => 'a:0:{}',
    'external' => 0,
    'rediscover' => 0,
    'weight' => 0,
    'expanded' => 0,
    'changed' => 1579555997,
    'default_langcode' => 0,
  ])
  ->execute();

$connection->insert('menu_link_content')
  ->fields([
    'id' => 998,
    'bundle' => 'menu_link_content',
    'uuid' => 'ea32f399-b53b-416c-81a9-e66204236c98',
    'langcode' => 'en',
  ])
  ->execute();
$connection->insert('menu_link_content_data')
  ->fields([
    'id' => 998,
    'bundle' => 'menu_link_content',
    'langcode' => 'en',
    'enabled' => 1,
    'title' => 'menu_link_998',
    'menu_name' => 'test-menu',
    'link__uri' => 'https://drupal.org',
    'link__title' => '',
    'link__options' => 'a:0:{}',
    'external' => 0,
    'rediscover' => 0,
    'weight' => 0,
    'expanded' => 0,
    'changed' => 1579555997,
    'default_langcode' => 0,
  ])
  ->execute();

$connection->insert('menu_link_content')
  ->fields([
    'id' => 999,
    'bundle' => 'menu_link_content',
    'uuid' => 'ea32f399-b53b-416c-81a9-e66204236c99',
    'langcode' => 'en',
  ])
  ->execute();
$connection->insert('menu_link_content_data')
  ->fields([
    'id' => 999,
    'bundle' => 'menu_link_content',
    'langcode' => 'en',
    'enabled' => 1,
    'title' => 'menu_link_999',
    'menu_name' => 'test-menu',
    'link__uri' => 'https://drupal.org',
    'link__title' => '',
    'link__options' => 'a:0:{}',
    'external' => 0,
    'rediscover' => 0,
    'weight' => 0,
    'expanded' => 0,
    'changed' => 1579555997,
    'default_langcode' => 0,
  ])
  ->execute();
$connection->insert('menu_link_content_data')
  ->fields([
    'id' => 999,
    'bundle' => 'menu_link_content',
    'langcode' => 'es',
    'enabled' => 1,
    'title' => 'menu_link_999-es',
    'menu_name' => 'test-menu',
    'link__uri' => 'https://drupal.org',
    'link__title' => '',
    'link__options' => 'a:0:{}',
    'external' => 0,
    'rediscover' => 0,
    'weight' => 0,
    'expanded' => 0,
    'changed' => 1579555997,
    'default_langcode' => 1,
  ])
  ->execute();
+48 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ class MenuLinkContentUpdateTest extends UpdatePathTestBase {
  protected function setDatabaseDumpFiles() {
    $this->databaseDumpFiles = [
      __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.filled.standard.php.gz',
      __DIR__ . '/../../../fixtures/update/drupal-8.menu-link-content-null-data-3056543.php',
    ];
  }

@@ -69,8 +70,26 @@ public function testConversionToRevisionable() {
    $entity_type = \Drupal::entityDefinitionUpdateManager()->getEntityType('menu_link_content');
    $this->assertFalse($entity_type->isRevisionable());

    // Set the batch size to 1 to test multiple steps.
    drupal_rewrite_settings([
      'settings' => [
        'update_sql_batch_size' => (object) [
          'value' => 1,
          'required' => TRUE,
        ],
      ],
    ]);

    // Check that there are broken menu links in the database tables, initially.
    $this->assertMenuLinkTitle(997, '');
    $this->assertMenuLinkTitle(998, '');
    $this->assertMenuLinkTitle(999, 'menu_link_999-es');

    $this->runUpdates();

    // Check that the update function returned the expected message.
    $this->assertSession()->pageTextContains('Custom menu links have been converted to be revisionable. 2 menu links with data integrity issues were restored. More details have been logged.');

    $entity_type = \Drupal::entityDefinitionUpdateManager()->getEntityType('menu_link_content');
    $this->assertTrue($entity_type->isRevisionable());

@@ -101,6 +120,35 @@ public function testConversionToRevisionable() {
    $this->assertEquals('Pineapple', $menu_link->label());
    $this->assertEquals('route:user.page', $menu_link->link->uri);
    $this->assertTrue($menu_link->isPublished());

    // Check that two menu links were restored and one was ignored. The latter
    // cannot be manually restored, since we would end up with two data table
    // records having "default_langcode" equalling 1, which would not make
    // sense.
    $this->assertMenuLinkTitle(997, 'menu_link_997');
    $this->assertMenuLinkTitle(998, 'menu_link_998');
    $this->assertMenuLinkTitle(999, 'menu_link_999-es');
  }

  /**
   * Assert that a menu link label matches the expectation.
   *
   * @param string $id
   *   The menu link ID.
   * @param string $expected_title
   *   The expected menu link title.
   */
  protected function assertMenuLinkTitle($id, $expected_title) {
    $database = \Drupal::database();
    $query = $database->select('menu_link_content_data', 'd');
    $query->join('menu_link_content', 'b', 'b.id = d.id AND d.default_langcode = 1');
    $title = $query
      ->fields('d', ['title'])
      ->condition('d.id', $id)
      ->execute()
      ->fetchField();

    $this->assertSame($expected_title, $title ?: '');
  }

  /**
+112 −1
Original line number Diff line number Diff line
@@ -7,7 +7,12 @@

use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\taxonomy\TermStorage;
use Drupal\views\ViewExecutable;

/**
@@ -146,6 +151,12 @@ function taxonomy_post_update_remove_hierarchy_from_vocabularies(&$sandbox = NUL
 * Update taxonomy terms to be revisionable.
 */
function taxonomy_post_update_make_taxonomy_term_revisionable(&$sandbox) {
  $finished = _taxonomy_post_update_make_taxonomy_term_revisionable__fix_default_langcode($sandbox);
  if (!$finished) {
    $sandbox['#finished'] = 0;
    return NULL;
  }

  $definition_update_manager = \Drupal::entityDefinitionUpdateManager();
  /** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository */
  $last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository');
@@ -230,5 +241,105 @@ function taxonomy_post_update_make_taxonomy_term_revisionable(&$sandbox) {

  $definition_update_manager->updateFieldableEntityType($entity_type, $field_storage_definitions, $sandbox);

  if (!empty($sandbox['data_fix']['default_langcode']['processed'])) {
    $count = $sandbox['data_fix']['default_langcode']['processed'];
    if (\Drupal::moduleHandler()->moduleExists('dblog')) {
      // @todo Simplify with https://www.drupal.org/node/2548095
      $base_url = str_replace('/update.php', '', \Drupal::request()->getBaseUrl());
      $args = [
        ':url' => Url::fromRoute('dblog.overview', [], ['query' => ['type' => ['update'], 'severity' => [RfcLogLevel::WARNING]]])
          ->setOption('base_url', $base_url)
          ->toString(TRUE)
          ->getGeneratedUrl(),
      ];
      return new PluralTranslatableMarkup($count, 'Taxonomy terms have been converted to be revisionable. One term with data integrity issues was restored. More details have been <a href=":url">logged</a>.', 'Taxonomy terms have been converted to be revisionable. @count terms with data integrity issues were restored. More details have been <a href=":url">logged</a>.', $args);
    }
    else {
      return new PluralTranslatableMarkup($count, 'Taxonomy terms have been converted to be revisionable. One term with data integrity issues was restored. More details have been logged.', 'Taxonomy terms have been converted to be revisionable. @count terms with data integrity issues were restored. More details have been logged.');
    }
  }
  else {
    return t('Taxonomy terms have been converted to be revisionable.');
  }
}

/**
 * Fixes recoverable data integrity issues in the "default_langcode" field.
 *
 * @param array $sandbox
 *   The update sandbox array.
 *
 * @return bool
 *   TRUE if the operation was finished, FALSE otherwise.
 *
 * @internal
 */
function _taxonomy_post_update_make_taxonomy_term_revisionable__fix_default_langcode(array &$sandbox) {
  if (!empty($sandbox['data_fix']['default_langcode']['finished'])) {
    return TRUE;
  }

  $storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
  if (!$storage instanceof TermStorage) {
    $sandbox['data_fix']['default_langcode']['finished'] = TRUE;
    return TRUE;
  }
  elseif (!isset($sandbox['data_fix']['default_langcode'])) {
    $sandbox['data_fix']['default_langcode'] = [
      'last_id' => 0,
      'processed' => 0,
    ];
  }

  $database = \Drupal::database();
  $data_table_name = 'taxonomy_term_field_data';
  $last_id = $sandbox['data_fix']['default_langcode']['last_id'];
  $limit = Settings::get('update_sql_batch_size', 200);

  // Detect records in the data table matching the base table language, but
  // having the "default_langcode" flag set to with 0, which is not supported.
  $query = $database->select($data_table_name, 'd');
  $query->leftJoin('taxonomy_term_data', 'b', 'd.tid = b.tid AND d.langcode = b.langcode AND d.default_langcode = 0');
  $result = $query->fields('d', ['tid', 'langcode'])
    ->condition('d.tid', $last_id, '>')
    ->isNotNull('b.tid')
    ->orderBy('d.tid')
    ->range(0, $limit)
    ->execute();

  foreach ($result as $record) {
    $sandbox['data_fix']['default_langcode']['last_id'] = $record->tid;

    // We need to exclude any term already having also a data table record with
    // the "default_langcode" flag set to 1, because this is a data integrity
    // issue that cannot be fixed automatically. However the latter will not
    // make the update fail.
    $has_default_langcode = (bool) $database->select($data_table_name, 'd')
      ->fields('d', ['tid'])
      ->condition('d.tid', $record->tid)
      ->condition('d.default_langcode', 1)
      ->range(0, 1)
      ->execute()
      ->fetchField();

    if ($has_default_langcode) {
      continue;
    }

    $database->update($data_table_name)
      ->fields(['default_langcode' => 1])
      ->condition('tid', $record->tid)
      ->condition('langcode', $record->langcode)
      ->execute();

    $sandbox['data_fix']['default_langcode']['processed']++;

    \Drupal::logger('update')
      ->warning('The term with ID @id had data integrity issues and was restored.', ['@id' => $record->tid]);
  }

  $finished = $sandbox['data_fix']['default_langcode']['last_id'] === $last_id;
  $sandbox['data_fix']['default_langcode']['finished'] = $finished;

  return $finished;
}
+104 −0
Original line number Diff line number Diff line
<?php

/**
 * @file
 * Contains database additions to drupal-8.filled.standard.php.gz for testing
 * the upgrade path of https://www.drupal.org/project/drupal/issues/3056543.
 */

use Drupal\Core\Database\Database;

$connection = Database::getConnection();

$connection->insert('taxonomy_term_data')
  ->fields([
    'tid' => 997,
    'vid' => 'tags',
    'uuid' => 'ea32f399-a53b-416c-81a9-e66204236c97',
    'langcode' => 'en',
  ])
  ->execute();
$connection->insert('taxonomy_term_field_data')
  ->fields([
    'tid' => 997,
    'vid' => 'tags',
    'langcode' => 'en',
    'default_langcode' => 0,
    'name' => 'tag997',
    'weight' => 0,
    'changed' => 1579555997,
    'content_translation_status' => NULL,
  ])
  ->execute();
$connection->insert('taxonomy_term_hierarchy')
  ->fields([
    'tid' => 997,
    'parent' => 0,
  ])
  ->execute();

$connection->insert('taxonomy_term_data')
  ->fields([
    'tid' => 998,
    'vid' => 'tags',
    'uuid' => 'ea32f399-a53b-416c-81a9-e66204236c98',
    'langcode' => 'en',
  ])
  ->execute();
$connection->insert('taxonomy_term_field_data')
  ->fields([
    'tid' => 998,
    'vid' => 'tags',
    'langcode' => 'en',
    'default_langcode' => 0,
    'name' => 'tag998',
    'weight' => 0,
    'changed' => 1579555998,
    'content_translation_status' => NULL,
  ])
  ->execute();
$connection->insert('taxonomy_term_hierarchy')
  ->fields([
    'tid' => 998,
    'parent' => 0,
  ])
  ->execute();

$connection->insert('taxonomy_term_data')
  ->fields([
    'tid' => 999,
    'vid' => 'tags',
    'uuid' => 'ea32f399-a53b-416c-81a9-e66204236c99',
    'langcode' => 'en',
  ])
  ->execute();
$connection->insert('taxonomy_term_field_data')
  ->fields([
    'tid' => 999,
    'vid' => 'tags',
    'langcode' => 'en',
    'default_langcode' => 0,
    'name' => 'tag999-en',
    'weight' => 0,
    'changed' => 1579555999,
    'content_translation_status' => NULL,
  ])
  ->execute();
$connection->insert('taxonomy_term_field_data')
  ->fields([
    'tid' => 999,
    'vid' => 'tags',
    'langcode' => 'es',
    'default_langcode' => 1,
    'name' => 'tag999-es',
    'weight' => 0,
    'changed' => 1579555999,
    'content_translation_status' => NULL,
  ])
  ->execute();
$connection->insert('taxonomy_term_hierarchy')
  ->fields([
    'tid' => 999,
    'parent' => 0,
  ])
  ->execute();
Loading