| Commit | Line | Data |
|---|---|---|
| f5d210e1 | 1 | <?php |
| f5d210e1 KS |
2 | /** |
| 3 | * @file content_migrate.admin.inc | |
| f7cb36f7 | 4 | * Code to process field data migration, moved into a separate file for efficiency. |
| f5d210e1 KS |
5 | */ |
| 6 | ||
| 7 | /** | |
| 70de9852 KS |
8 | * Determine which fields can be migrated, have already been migrated, and are |
| 9 | * unable to be migrated due to missing modules. | |
| f5d210e1 | 10 | */ |
| 70de9852 | 11 | function content_migrate_get_options() { |
| f5d210e1 KS |
12 | $options = array('available' => array(), 'converted' => array(), 'missing' => array()); |
| 13 | ||
| 14 | $field_values = content_migrate_get_field_values(); | |
| cc335bda | 15 | |
| f5d210e1 KS |
16 | if (empty($field_values)) { |
| 17 | drupal_set_message(t('There is no D6 field information in this database.')); | |
| 70de9852 | 18 | return FALSE; |
| f5d210e1 KS |
19 | } |
| 20 | ||
| 21 | $type_names = node_type_get_names(); | |
| 22 | $new_fields = array_keys(field_info_fields()); | |
| 23 | ||
| 24 | // Figure out which field and widget modules are available. | |
| 8376bb8b | 25 | $available_modules = array_unique(array_merge(module_implements('field_info'), module_implements('field_widget_info'))); |
| f5d210e1 KS |
26 | |
| 27 | foreach ($field_values as $field_name => $field_value) { | |
| cc335bda | 28 | $messages = array(); |
| f5d210e1 KS |
29 | $bundles = array(); |
| 30 | $missing_module = !in_array($field_value['module'], $available_modules); | |
| cc335bda KS |
31 | if ($missing_module) { |
| 32 | $messages[] = '<span class="error">' . t("Missing field module: '@field'. This field cannot be migrated.", array('@field' => $field_value['module'])) . '</span>'; | |
| 33 | } | |
| 34 | if (isset($field_value['messages'])) { | |
| 35 | $messages = array_merge($messages, $field_value['messages']); | |
| 36 | unset($field_value['messages']); | |
| 37 | } | |
| 38 | ||
| f5d210e1 | 39 | $instance_values = content_migrate_get_instance_values(NULL, $field_name); |
| cc335bda | 40 | |
| 0373a5db | 41 | |
| 8376bb8b KS |
42 | // Debug |
| 43 | //dsm($field_value); | |
| 44 | //dsm($instance_values); | |
| 45 | ||
| f5d210e1 | 46 | foreach ($instance_values as $bundle => $instance_value) { |
| cc335bda KS |
47 | if (isset($instance_value['messages'])) { |
| 48 | $messages = array_merge($messages, $instance_value['messages']); | |
| 49 | unset($instance_values[$bundle]['messages']); | |
| 50 | } | |
| f5d210e1 KS |
51 | $bundles[] = $type_names[$bundle]; |
| 52 | $label = $instance_value['label']; | |
| f5d210e1 KS |
53 | } |
| 54 | $data = array( | |
| 55 | 0 => $field_name, | |
| 56 | 1 => $field_value['type'], | |
| cc335bda KS |
57 | 2 => theme('item_list', array('items' => $bundles)), |
| 58 | 3 => theme('item_list', array('items' => $messages)), | |
| f5d210e1 KS |
59 | ); |
| 60 | if (in_array($field_name, $new_fields)) { | |
| 61 | $options['converted'][$field_name] = $data; | |
| 62 | } | |
| 63 | // TODO, do we need to check for more than the mere presence of a module? | |
| 64 | elseif ($missing_module) { | |
| 65 | $options['missing'][$field_name] = $data; | |
| 66 | } | |
| 67 | else { | |
| 68 | $options['available'][$field_name] = $data; | |
| 69 | } | |
| 70 | } | |
| 70de9852 KS |
71 | return $options; |
| 72 | } | |
| 73 | ||
| 74 | /** | |
| 75 | * Form generator for the migration selection form. | |
| 76 | * | |
| 77 | * @todo Make this into a nice table where you have | |
| 78 | * an option to check all available fields to migrate | |
| 79 | * them all at once. | |
| 80 | */ | |
| 81 | function content_migrate_select($form, &$form_state) { | |
| 82 | $form = array(); | |
| 83 | $options = content_migrate_get_options(); | |
| 84 | if (!$options) { | |
| 85 | return $form; | |
| 86 | } | |
| f5d210e1 | 87 | |
| 8376bb8b | 88 | $header = array(t('Field'), t('Field type'), t('Content type(s)'), t('Other information')); |
| f5d210e1 KS |
89 | $form['#tree'] = TRUE; |
| 90 | $form['available'] = array( | |
| 91 | '#type' => 'fieldset', | |
| 92 | '#collapsible' => TRUE, | |
| 93 | '#collapsed' => count($options['available']) < 1, | |
| 94 | '#title' => t('Available fields'), | |
| cc335bda | 95 | '#description' => t('Fields that have not yet been migrated but are available for migration. <strong>Please carefully read the messages next to each field before migrating it to understand changes that might be made.</strong>'), |
| f5d210e1 KS |
96 | ); |
| 97 | $form['available']['data'] = array( | |
| 98 | '#type' => 'tableselect', | |
| 99 | '#header' => $header, | |
| 100 | '#options' => $options['available'], | |
| 101 | '#empty' => t('No fields are available to be migrated.'), | |
| 102 | ); | |
| 103 | $form['available']['submit'] = array( | |
| 104 | '#type' => 'submit', | |
| 105 | '#value' => t('Migrate selected fields'), | |
| 106 | '#submit' => array('content_migrate_select_submit'), | |
| 107 | ); | |
| 108 | ||
| 109 | $form['converted'] = array( | |
| 110 | '#type' => 'fieldset', | |
| 111 | '#collapsible' => TRUE, | |
| 112 | '#collapsed' => count($options['converted']) < 1, | |
| 113 | '#title' => t('Converted fields'), | |
| 114 | '#description' => '<p>'. t('Fields that have already been converted. You can choose to roll them back if the conversion did not work correctly. Note that rolling fields back will completely destroy the new field tables.') . ' <span class="error"><strong>' . t('This operation cannot be undone!') . '</strong></span>', | |
| 115 | ); | |
| 116 | $form['converted']['data'] = array( | |
| 117 | '#type' => 'tableselect', | |
| 118 | '#header' => $header, | |
| 119 | '#options' => $options['converted'], | |
| 120 | '#empty' => t('No fields are already converted.'), | |
| 121 | ); | |
| 122 | $form['converted']['submit'] = array( | |
| 123 | '#type' => 'submit', | |
| 124 | '#value' => t('Roll back selected fields'), | |
| 125 | '#submit' => array('content_migrate_rollback_submit'), | |
| 126 | ); | |
| 127 | ||
| 128 | $form['missing'] = array( | |
| 129 | '#type' => 'fieldset', | |
| 130 | '#collapsible' => TRUE, | |
| 131 | '#collapsed' => count($options['missing']) < 1, | |
| 132 | '#title' => t('Unavailable fields'), | |
| 133 | '#description' => t('Fields that cannot be migrated because some modules are missing.'), | |
| 134 | ); | |
| 135 | $form['missing']['data'] = array( | |
| 136 | '#type' => 'tableselect', | |
| 137 | '#header' => $header, | |
| 138 | '#options' => $options['missing'], | |
| 139 | '#empty' => t('No fields have missing modules.'), | |
| 140 | ); | |
| 141 | ||
| 142 | return $form; | |
| 143 | } | |
| 144 | ||
| 145 | /** | |
| 146 | * Submit handler. | |
| 147 | * | |
| 148 | * @TODO add a confirmation on the rollback submission. | |
| 149 | */ | |
| 150 | function content_migrate_rollback_submit($form, &$form_state) { | |
| f5d210e1 | 151 | $field_names = array_filter($form_state['values']['converted']['data']); |
| 9067b59a | 152 | content_migrate_rollback($field_names); |
| e44c4441 KS |
153 | } |
| 154 | ||
| 155 | /** | |
| 156 | * Helper function to perform rollback. | |
| 157 | */ | |
| 158 | function content_migrate_rollback($field_names) { | |
| f5d210e1 KS |
159 | foreach ($field_names as $field_name) { |
| 160 | $field = field_info_field($field_name); | |
| 161 | ||
| 162 | // Deleting the field only marks it for deletion. | |
| 163 | field_delete_field($field_name); | |
| 164 | ||
| 165 | // We are bypassing the field batch processing | |
| 166 | // and simply deleting all the data. | |
| 167 | // The assumption is that the migration was | |
| 168 | // unsuccessful and will be re-attempted | |
| 169 | // and we need to remove all traces of the | |
| 170 | // new field for later migrations to work. | |
| 171 | $new_table = content_migrate_new_table($field); | |
| b338b91f | 172 | db_drop_table($new_table); |
| f5d210e1 KS |
173 | |
| 174 | $instances = field_read_instances(array('field_id' => $field['id']), array('include_deleted' => 1)); | |
| 175 | foreach ($instances as $instance) { | |
| 176 | field_purge_instance($instance); | |
| 177 | } | |
| 178 | field_purge_field($field); | |
| 179 | drupal_set_message(t('Rolling back @field_name.', array('@field_name' => $field_name))); | |
| 180 | } | |
| 181 | } | |
| 182 | ||
| 183 | /** | |
| 184 | * Submit handler. | |
| 185 | */ | |
| 186 | function content_migrate_select_submit($form, &$form_state) { | |
| 187 | $field_names = array_filter($form_state['values']['available']['data']); | |
| 188 | _content_migrate_batch($field_names); | |
| 189 | } | |
| 190 | ||
| 191 | /** | |
| 192 | * Helper function to create a batch. | |
| 193 | */ | |
| 194 | function _content_migrate_batch($field_names) { | |
| 195 | $batch = array( | |
| 196 | 'title' => t('Migrating data'), | |
| f7cb36f7 | 197 | 'file' => drupal_get_path('module', 'content_migrate') . '/includes/content_migrate.admin.inc', |
| f73fc580 | 198 | 'operations' => array(), |
| f5d210e1 KS |
199 | 'finished' => "Field migration is finished", |
| 200 | 'init_message' => t("Fields migration is starting."), | |
| 201 | 'progress_message' => t('Processed @current out of @total.'), | |
| 202 | 'error_message' => t('Field migration has encountered an error.'), | |
| 203 | ); | |
| 204 | // Migrate field data one field at a time. | |
| 205 | foreach ($field_names as $field_name) { | |
| f73fc580 | 206 | $batch['operations'][] = array('_content_migrate_batch_process_create_fields', array($field_name)); |
| f5d210e1 KS |
207 | $batch['operations'][] = array('_content_migrate_batch_process_migrate_data', array($field_name)); |
| 208 | } | |
| 209 | batch_set($batch); | |
| 210 | } | |
| 211 | ||
| 212 | /** | |
| 213 | * Batch operation callback to create fields. | |
| 214 | */ | |
| f73fc580 | 215 | function _content_migrate_batch_process_create_fields($field_name, &$context) { |
| cc335bda KS |
216 | $messages = array(); |
| 217 | $errors = array(); | |
| f5d210e1 | 218 | $type_names = node_type_get_names(); |
| c4e54e0e KS |
219 | $allowed_fields = field_info_field_types(); |
| 220 | $allowed_widgets = field_info_widget_types(); | |
| 221 | $allowed_formatters = field_info_formatter_types(); | |
| 222 | ||
| f73fc580 KS |
223 | $context['message'] = t('"Creating field: %field', array('%field' => $field_name)); |
| 224 | $field_value = content_migrate_get_field_values($field_name); | |
| cc335bda KS |
225 | if (isset($field_value['messages'])) { |
| 226 | $messages = $field_value['messages']; | |
| 227 | unset($field_value['messages']); | |
| 228 | } | |
| df844aae | 229 | $instance_info = field_info_instances('node'); |
| 59ea776a | 230 | |
| f73fc580 KS |
231 | // Create the field and store the new field |
| 232 | // definition in $context so we can retrieve it later. | |
| 233 | try { | |
| 234 | // A shared field may already have been created, check first. | |
| df844aae | 235 | $field = field_info_field($field_value['field_name']); |
| f73fc580 KS |
236 | if (empty($field)) { |
| 237 | unset($field_value['columns']); | |
| 238 | unset($field_value['db_storage']); | |
| 239 | $field = field_create_field($field_value); | |
| 240 | $context['fields'][$field_name] = $field; | |
| cc335bda | 241 | $messages[] = t("Created field @field_name", array('@field_name' => $field_name)); |
| f73fc580 | 242 | } |
| bccb3da9 | 243 | |
| f73fc580 KS |
244 | // Create each of the new instances and store |
| 245 | // the new instance definitions in $context. | |
| 246 | $instance_values = content_migrate_get_instance_values(NULL, $field_name); | |
| 59ea776a | 247 | |
| f73fc580 KS |
248 | foreach ($instance_values as $bundle => $instance_value) { |
| 249 | try { | |
| c4e54e0e | 250 | |
| cc335bda KS |
251 | if (isset($instance_value['messages'])) { |
| 252 | $messages = array_merge($messages, $instance_value['messages']); | |
| 253 | unset($instance_value['messages']); | |
| c4e54e0e KS |
254 | } |
| 255 | ||
| df844aae KS |
256 | if (!isset($instance_info[$bundle][$field_name])) { |
| 257 | $instance = field_create_instance($instance_value); | |
| 258 | $context['instances'][$field_name][$bundle] = $instance; | |
| cc335bda KS |
259 | $messages[] = t("Created instance of @field_name in bundle @bundle.", array( |
| 260 | '@field_name' => $field_name, '@bundle' => $type_names[$bundle])); | |
| df844aae | 261 | } |
| f73fc580 KS |
262 | |
| 263 | } | |
| 264 | catch (Exception $e) { | |
| cc335bda KS |
265 | $errors[] = t('Error creating instance of @field_name in bundle @bundle.', array( |
| 266 | '@field_name' => $field_name, '@bundle' => $type_names[$bundle])); | |
| 267 | $errors[] = $e; | |
| f5d210e1 KS |
268 | } |
| 269 | } | |
| f5d210e1 | 270 | } |
| f73fc580 | 271 | catch (Exception $e) { |
| cc335bda KS |
272 | $errors[] = t("Error creating field @field_name", array('@field_name' => $field_name)); |
| 273 | $errors[] = $e; | |
| f73fc580 KS |
274 | } |
| 275 | field_info_cache_clear(); | |
| 276 | ||
| cc335bda KS |
277 | foreach ($messages as $message) { |
| 278 | drupal_set_message($message, 'warning'); | |
| 279 | } | |
| 280 | foreach ($errors as $error) { | |
| 281 | drupal_set_message($error, 'error'); | |
| 282 | } | |
| 283 | ||
| f5d210e1 KS |
284 | $context['finished'] = TRUE; |
| 285 | } | |
| 286 | ||
| 287 | /** | |
| 288 | * Batch operation callback to migrate data. | |
| 289 | * Copy old table data to new field table. | |
| f5d210e1 | 290 | */ |
| 763b3b8c KS |
291 | function _content_migrate_batch_process_migrate_data($field_name, &$context) { |
| 292 | ||
| 293 | // The first time through, find all the nodes that have this field. | |
| 294 | if (!isset($context['sandbox']['progress'])) { | |
| 295 | ||
| 296 | $field_value = content_migrate_get_field_values($field_name); | |
| cc335bda KS |
297 | if (isset($field_value['messages'])) { |
| 298 | unset($field_value['messages']); | |
| 299 | } | |
| 763b3b8c | 300 | $instance_values = content_migrate_get_instance_values(NULL, $field_name); |
| cc335bda KS |
301 | if (isset($instance_values['messages'])) { |
| 302 | unset($instance_values['messages']); | |
| 303 | } | |
| 763b3b8c KS |
304 | $types = array(); |
| 305 | foreach ($instance_values as $bundle => $instance_value) { | |
| 306 | $types[] = $bundle; | |
| 307 | } | |
| 308 | $field = field_info_field($field_name); | |
| f5d210e1 KS |
309 | $old_table = content_migrate_old_table($field_value, $instance_value); |
| 310 | $old_cols = content_migrate_old_columns($field_value, $instance_value); | |
| 311 | $new_table = content_migrate_new_table($field); | |
| 312 | $new_revision_table = content_migrate_new_revision($field); | |
| 313 | $new_columns = content_migrate_new_columns($field); | |
| 763b3b8c KS |
314 | // Shared, non-multiple fields do not have a delta but are still in per-field tables. |
| 315 | $add_delta = $field_value['cardinality'] != 1 && content_migrate_storage_type($field_value, $instance_value) == CONTENT_DB_STORAGE_PER_FIELD; | |
| 316 | ||
| 317 | $query = db_select($old_table, 'old_table', array('fetch' => PDO::FETCH_ASSOC)); | |
| 318 | $node_alias = $query->join('node', 'n', 'old_table.nid=n.nid'); | |
| 319 | $result = $query | |
| 819e88cc | 320 | ->fields($node_alias, array('title', 'type', 'vid', 'language')) |
| 763b3b8c KS |
321 | ->fields('old_table', array('nid')) |
| 322 | ->orderBy('nid', 'ASC') | |
| 8043533c | 323 | ->distinct() |
| 763b3b8c | 324 | ->execute(); |
| 8376bb8b | 325 | |
| 763b3b8c KS |
326 | $nodes = array(); |
| 327 | foreach ($result as $row) { | |
| 819e88cc | 328 | $nodes[] = array('nid' => $row['nid'], 'title' => $row['title'], 'type' => $row['type'], 'vid' => $row['vid'], 'language' => $row['language']); |
| 763b3b8c KS |
329 | } |
| 330 | $context['sandbox']['progress'] = 0; | |
| 331 | $context['sandbox']['max'] = count($nodes); | |
| 332 | $context['sandbox']['nodes'] = $nodes; | |
| 333 | $context['sandbox']['old_table'] = $old_table; | |
| 334 | $context['sandbox']['new_table'] = $new_table; | |
| 335 | $context['sandbox']['new_revision_table'] = $new_revision_table; | |
| 336 | $context['sandbox']['old_cols'] = $old_cols; | |
| 337 | $context['sandbox']['new_cols'] = $new_columns; | |
| 338 | $context['sandbox']['types'] = $types; | |
| 339 | $context['sandbox']['field'] = $field; | |
| 340 | $context['sandbox']['add_delta'] = $add_delta; | |
| 341 | ||
| 342 | } | |
| 343 | ||
| 344 | // Process one node in each batch. | |
| 345 | ||
| 346 | $node = array_shift($context['sandbox']['nodes']); | |
| 680215e8 KS |
347 | if (!$node) { |
| 348 | return; | |
| 349 | } | |
| 763b3b8c | 350 | |
| 575ce50d KS |
351 | $field = field_info_field($field_name); |
| 352 | $instance = field_info_instance('node', $field_name, $node['type']); | |
| ce009a02 | 353 | |
| 763b3b8c KS |
354 | // Construct an record to insert into the new field table |
| 355 | // from the data in the old table. | |
| 356 | ||
| 357 | $query = db_select($context['sandbox']['old_table'], 'old_table', array('fetch' => PDO::FETCH_ASSOC)); | |
| f5d210e1 | 358 | |
| 763b3b8c | 359 | // We need new columns for bundle name, entity type, and language. |
| 0373a5db KS |
360 | // See the debate going on at http://drupal.org/node/1164852. |
| 361 | // Reverting back to setting all nodes as untranslated. | |
| 362 | $language = LANGUAGE_NONE; | |
| 363 | if ($field['translatable']) { | |
| 364 | //$language = $node['language']; | |
| 365 | } | |
| 763b3b8c KS |
366 | $query->addExpression("'". $node['type'] ."'", 'bundle'); |
| 367 | $query->addExpression("'node'", 'entity_type'); | |
| 0373a5db | 368 | $query->addExpression("'". $language ."'", 'language'); |
| f5d210e1 | 369 | |
| 763b3b8c KS |
370 | // There are new names for what were the nid and vid columns. |
| 371 | $query->addField('old_table', 'nid', 'entity_id'); | |
| 372 | $query->addField('old_table', 'vid', 'revision_id'); | |
| f5d210e1 | 373 | |
| 763b3b8c KS |
374 | // Add the field columns to the select query. |
| 375 | // Use the new column names as aliases in case the | |
| 376 | // name changed, hopefully none did. | |
| df844aae KS |
377 | foreach ($context['sandbox']['old_cols'] as $column_name => $db_column_name) { |
| 378 | $query->addField('old_table', $db_column_name, $context['sandbox']['new_cols'][$column_name]); | |
| 763b3b8c | 379 | } |
| f5d210e1 | 380 | |
| 763b3b8c KS |
381 | // Add delta, or construct it if missing. |
| 382 | if ($context['sandbox']['add_delta']) { | |
| 383 | $query->addField('old_table', 'delta', 'delta'); | |
| 384 | } | |
| 385 | else { | |
| 386 | $query->addExpression(0, 'delta'); | |
| 387 | } | |
| 388 | $query->condition('nid', $node['nid']); | |
| 389 | $result = $query->execute(); | |
| bccb3da9 | 390 | |
| 763b3b8c KS |
391 | foreach ($result as $record) { |
| 392 | ||
| 393 | // Let modules alter this before the insert. | |
| ce009a02 | 394 | drupal_alter('content_migrate_data_record', $record, $field, $instance); |
| 763b3b8c | 395 | |
| d28bbf01 KS |
396 | // Don't save empty values. |
| 397 | if (!empty($record)) { | |
| 398 | $function = $field['module'] . '_field_is_empty'; | |
| df844aae KS |
399 | if (function_exists($function)) { |
| 400 | // The $record array has the database columns as keys, which drupal_write_record() will need, | |
| 401 | // but the _field_is_empty() function will be looking for the short, normalized column name. | |
| 402 | $item = array(); | |
| 403 | foreach ($context['sandbox']['new_cols'] as $column_name => $db_column_name) { | |
| 404 | if (array_key_exists($db_column_name, $record)) { | |
| 405 | $item[$column_name] = $record[$db_column_name]; | |
| 406 | } | |
| 407 | } | |
| 408 | if ($function($item, $field)) { | |
| 409 | $record = NULL; | |
| 410 | } | |
| d28bbf01 KS |
411 | } |
| 412 | } | |
| 413 | ||
| bdf8319f | 414 | if (!empty($record)) { |
| f5df077d KS |
415 | if ($record['revision_id'] == $node['vid']) { |
| 416 | drupal_write_record($context['sandbox']['new_table'], $record); | |
| 417 | } | |
| bdf8319f KS |
418 | drupal_write_record($context['sandbox']['new_revision_table'], $record); |
| 419 | } | |
| 763b3b8c KS |
420 | } |
| 421 | ||
| 422 | // Update our progress information. | |
| 423 | $context['sandbox']['progress']++; | |
| 424 | $context['message'] = t('Processing %nid : %title', array('%title' => $node['title'], '%nid' => $node['nid'])); | |
| 425 | ||
| 426 | // Inform the batch engine that we are not finished, | |
| 427 | // and provide an estimation of the completion level we reached. | |
| 428 | if ($context['sandbox']['progress'] != $context['sandbox']['max']) { | |
| 429 | $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; | |
| f5d210e1 | 430 | } |
| 763b3b8c | 431 | |
| 6fb49f76 | 432 | } |
| 763b3b8c KS |
433 | |
| 434 |