<?php
// $Id$

/**
 *  @file
 *  Adds capability to import/export CCK field data definitions.
 *
 *  Emulates the import/export process introduced in Views to export the field settings arrays as text
 *  them copy and paste text to import the field settings back into another content type.
 *
 *  Functions to create and playback macros borrowed from moshe weitzman's macro module.
 *  Macros created using drupal_execute() on the field settings form for each of the requested forms.
 *  Multi-part forms built using examples from Jeff Eaton's example at
 *  http://jeff.viapositiva.net/drupal/dynamic-forms.
 *
 *  You can export one or two fields and/or groups from one content type and import them into
 *  another content type in the same or a different installation,
 *  or export a complete content type with all groups and fields and create it
 *  as an exact copy in another installation.
 *
 *  Content type, group and field names will be imported exactly as exported.
 *  If the names are already in use, no import will be performed.
 *
 *
 *  Note: The "display fields" information is being handled a little differently than the rest of the
 *  data that's imported and exported.  Instead of calling through the create and playback macros,
 *  we get and set the data directly from/into the database.  the reason for this is that the
 *  playback macro method does not lend itself well to the display fields.
 */

/**
 * Implementation of hook_menu().
 */
function content_copy_menu() {
  $items = array();

  $items['admin/content/types/export'] = array(
    'title' => 'Export',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('content_copy_export_form'),
    'access arguments' => array('administer content types'),
    'type' => MENU_LOCAL_TASK,
    'weight' => 3,
  );
  $items['admin/content/types/import'] = array(
    'title' => 'Import',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('content_copy_import_form'),
    'access arguments' => array('administer content types'),
    'type' => MENU_LOCAL_TASK,
    'weight' => 4,
  );
  return $items;
}

/**
 * Implementation of hook_theme().
 */
function content_copy_theme() {
  return array(
    'content_copy_export_form' => array(
      'template' => 'content_copy_export_form',
      'arguments' => array('form' => NULL),
    ),
  );
}

/**
 *  A form to export field definitions.
 */
function content_copy_export_form(&$form_state) {
  include_once('./'. drupal_get_path('module', 'content') .'/includes/content.admin.inc');
  include_once('./'. drupal_get_path('module', 'node') .'/content_types.inc');

  $form_values = isset($form_state['values']) ? $form_state['values'] : array();
  $step = isset($form_state['storage']['step']) ? $form_state['storage']['step'] + 1 : 1;

  $exportable_fields = array();
  $groups = array();

  $type_name = isset($form_values['type_name']) ? $form_values['type_name'] : '';
  if ($type_name) {
    $type = content_types($type_name);
    $exportable_fields = content_copy_fields($type_name);
    if (module_exists('fieldgroup')) {
      $groups = fieldgroup_groups($type_name);
    }
  }

  // If a content type has been selected and there are no fields or groups to select,
  // jump straight to export.
  if ($step == 2 && !($groups) && !($exportable_fields)) {
    $step = 3;
  }

  $form['#step'] = $step;
  $form['#prefix'] = t('This form will process a content type and one or more fields from that type and export the settings. The export created by this process can be copied and pasted as an import into the current or any other database. The import will add the fields to an existing content type or create a new content type that includes the selected fields.');

  switch ($step) {
    case 1: // Select a content type.
      $types  = content_copy_types();
      $form['type_name'] = array(
        '#title' => t('Types'),
        '#type' => 'radios',
        '#options' => $types,
        '#description' => t('Select the content type to export.'),
      );
      break;

    case 2: // Select groups and fields.
      $form['type_name'] = array(
        '#type' => 'hidden',
        '#value' => $type_name,
      );

      $form += array(
        '#fields' => $exportable_fields,
        '#groups' => array_keys($groups),
      );

      $fields_options = $groups_options = array();

      // Fields.
      foreach ($exportable_fields as $field_name) {
        $field = content_fields($field_name, $type_name);
        $fields_options[$field_name] = '';
        $weight = $field['widget']['weight'];
        $form[$field_name] = array(
          'human_name' => array('#value' => check_plain($field['widget']['label'])),
          'field_name' => array('#value' => $field['field_name']),
          'type' => array('#value' => $field['type']),
          'weight' => array('#type' => 'value', '#value' => $weight),
          'parent' => array('#type' => 'value', '#value' => ''),
          '#row_type' => 'field',
        );
      }
      $form['fields'] = array(
        '#type' => 'checkboxes',
        '#options' => $fields_options,
        '#default_value' => array_keys($fields_options),
      );

      // Groups.
      foreach ($groups as $name => $group) {
        $groups_options[$name] = '';
        $weight = $group['weight'];
        $form[$name] = array(
          'human_name' => array('#value' => check_plain($group['label'])),
          'group_name' => array('#value' => $group['group_name']),
          'weight' => array('#type' => 'value', '#value' => $weight),
          '#row_type' => 'group',
        );
        foreach ($group['fields'] as $field_name => $field) {
          // Do nothing for non-exportable (inactive) fields.
          if (isset($form[$field_name])) {
            $form[$field_name]['parent']['#value'] = $name;
          }
        }
      }
      if ($groups) {
        $form['groups'] = array(
          '#type' => 'checkboxes',
          '#options' => $groups_options,
          '#default_value' => array_keys($groups_options),
        );
      }
      break;

    case 3: // Display the export macro.
      $GLOBALS['content_copy']['count'] = 0;
      $form['export'] = array(
        '#title' => t('Export data'),
        '#type' => 'textarea',
        '#cols' => 60,
        '#value' => content_copy_export($form_values),
        '#rows' => max(40, $GLOBALS['content_copy']['count']),
        '#description' => t('Copy the export text and paste it into another content type using the import function.'),
      );
      // The calls to drupal_execute('content_field_edit_form') in
      // content_copy_export() affect the page title,
      drupal_set_title(t('Content types'));
      break;
  }

  if ($step < 3) {  // Omit submit button on the textarea block to display the export data.
    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Export'),
    );
  }

  $form['step'] = array(
    '#type' => 'value',
    '#value' => $step,
  );

  return $form;
}

function content_copy_export_form_submit($form, &$form_state) {
  $form_state['rebuild'] = TRUE;
  $form_state['storage']['step'] = $form_state['values']['step'];
}


/**
 *  Process the export, get field admin forms for all requested fields
 *   and save the form values as formatted text.
 */
function content_copy_export($form_values) {
  // Set a global variable to tell when to intervene with form_alter().
  $GLOBALS['content_copy']['status'] = 'export';

  // Get the content type info by submitting the content type form.
  $node_state = array('values' => array('type_name' => $form_values['type_name']));
  module_load_include('inc', 'node', 'content_types');
  drupal_execute('node_type_form', $node_state, node_get_types('type', $form_values['type_name']));

  module_load_include('inc', 'content', 'includes/content.admin');
  module_load_include('inc', 'content', 'includes/content.crud');

  // Get an array of groups to export.
  // Record a macro for each group by submitting the group edit form.
  $groups = array();
  if (!empty($form_values['groups']) && module_exists('fieldgroup')) {
    $groups = array_filter($form_values['groups']);
    foreach ($groups as $group) {
      $group_state = array('values' => array('group_name' => $group));
      drupal_execute('fieldgroup_group_edit_form', $group_state, $form_values['type_name'], $group, 'edit');
    }
  }

  // Get an array of fields to export
  // Record a macro for each field by submitting the field settings form.
  // Omit fields from the export if their module is not currently installed
  // otherwise the system will generate errors when the macro tries to execute their forms.
  if (!empty($form_values['fields'])) {
    $type = content_types($form_values['type_name']);
    $fields = array_filter($form_values['fields']);
    foreach ($fields as $field_name) {
      $field = $type['fields'][$field_name];
      $field_types = _content_field_types();
      $field_module = $field_types[$field['type']]['module'];
      $widget_types = _content_widget_types();
      $widget_module = $widget_types[$field['widget']['type']]['module'];
      if (!empty($field_module) && module_exists($field_module) && !empty($widget_module) && module_exists($widget_module)) {
        $field_state = array('values' => content_field_instance_collapse($field));
        $field_state['values']['op'] = t('Save field settings');
        if (module_exists('fieldgroup')) {
          // Avoid undefined index error by always creating this.
          $field_state['values']['group'] = '';
          $group_name = fieldgroup_get_group($form_values['type_name'], $field_name);
          if (in_array($group_name, $groups)) {
            $field_state['values']['group'] = $group_name;
          }
        }
        drupal_execute('content_field_edit_form', $field_state, $form_values['type_name'], $field_name);
      }
    }
  }

  // Convert the macro array into formatted text.
  $output = content_copy_get_macro();

  // Add weights of non-CCK fields.
  if ($extra = variable_get('content_extra_weights_'. $form_values['type_name'], array())) {
    $output .= "\$content['extra']  = ". var_export((array) $extra, TRUE) .";\n";
  }

  return $output;
}

/**
 *  A form to import formatted text created with export.
 *
 * The macro can be filled from a file, if provided.
 * Provide a type_name to force the fields to be added to a specific
 * type, or leave out type_name to create a new content type.
 *
 * Example:
 * // If Content Copy is enabled, offer an import link.
 * if (module_exists('content_copy')) {
 *   $form['macro'] = array(
 *     '#type' => 'fieldset',
 *     '#title' => t('Create a content type'),
 *     '#description' => t('Follow this link to create automatically a content type and import preconfigured fields.'),
 *     '#collapsible' => TRUE,
 *     '#collapsed' => FALSE,
 *   );
 *   $form['macro']['link'] = array(
 *     '#type' => 'markup',
 *     '#value' => l(t('import'), 'admin/content/types/import', array(), 'type_name=event&macro_file='. drupal_get_path('module', 'my_module') .'/my_content_type.txt'),
 *   );
 * }
 */
function content_copy_import_form(&$form_state, $type_name = '') {
  include_once('./'. drupal_get_path('module', 'content') .'/includes/content.admin.inc');
  include_once('./'. drupal_get_path('module', 'node') .'/content_types.inc');

  $form['#prefix'] = t('This form will import field definitions exported from another content type or another database.<br/>Note that fields cannot be duplicated within the same content type, so imported fields will be added only if they do not already exist in the selected type.');
  $form['type_name'] = array(
    '#type' => 'select',
    '#options' => array('<create>' => t('<Create>')) + content_copy_types(),
    '#default_value' => $type_name,
    '#title' => t('Content type'),
    '#description' => t('Select the content type to import these fields into.<br/>Select &lt;Create&gt; to create a new content type to contain the fields.'),
  );
  $form['macro'] = array(
    '#type' => 'textarea',
    '#rows' => 40,
    '#title' => t('Import data'),
    '#required' => TRUE,
    '#description' => t('Paste the text created by a content export into this field.'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Import'),
  );
  // Read in a file if there is one and set it as the default macro value.
  if (isset($_REQUEST['macro_file']) && $file = file_get_contents($_REQUEST['macro_file'])) {
    $form['macro']['#default_value'] = $file;
    if (isset($_REQUEST['type_name'])) {
      $form['type_name']['#default_value'] = $_REQUEST['type_name'];
    }
    $form['#prefix'] .= '<p class="error">'. t('A file has been pre-loaded for import.') .'</p>';
  }
  return $form;
}

/**
 *  Submit handler for import form.
 *   For each submitted field:
 *     1) add new field to the database
 *     2) execute the imported field macro to update the settings to the imported values
 */
function content_copy_import_form_submit($form, &$form_state) {
  $form_values = $form_state['values'];

  // Get the content type we are importing into.
  $type_name = $form_values['type_name'];
  $type_label = node_get_types('name', $type_name);

  $content = NULL;
  // Convert the import formatted text back into a $content array.
  // Return if errors generated or not an array.
  // Use '@' to suppress errors about undefined constants in the macro.
  @eval($form_values['macro']);

  // Preliminary error trapping, must have valid arrays to work with.
  if (!isset($content) || !isset($content['type']) || !is_array($content) || !is_array($content['type'])) {
    form_set_error('macro', t('The import data is not valid import text.'));
    return;
  }

  module_load_include('inc', 'content', 'includes/content.crud');

  // Get all type and field info for this database.
  $content_info = _content_type_info();

  $imported_type = $content['type'];
  $imported_type_name = $imported_type['type'];
  $imported_type_label = $imported_type['name'];

  // It is allowed to import a type with no fields,
  // so the fields array could be empty and must be cast as an array.
  $imported_fields = isset($content['fields']) ? $content['fields'] : array();

  // Perform more pre-import error trapping.
  // If there are potential problems, exit without doing the import.
  $not_enabled = array();

  // The groups array could be empty and still valid, make sure to cast it as an array.
  // If there are groups in the import, make sure the fieldgroup module is enabled.
  $imported_groups = array();
  if (isset($content['groups']) && module_exists('fieldgroup')) {
    $imported_groups = (array) $content['groups'];
  }
  elseif (isset($content['groups']) && is_array($content['groups'])) {
    $not_enabled[] = 'fieldgroup';
  }

  // Make sure that all the field and widget modules in the import are enabled in this database.
  foreach ($imported_fields as $import) {
    $field = content_field_instance_collapse($import);
    if (empty($field['module']) || empty($field['widget_module'])) {
      $not_enabled[] = $field['field_name'];
    }
    else {
      if (!module_exists($field['module'])) {
        $not_enabled[] = $field['module'];
      }
      if (!module_exists($field['widget_module'])) {
        $not_enabled[] = $field['widget_module'];
      }
    }
  }

  // If any required module is not enabled, set an error message and exit.
  if ($not_enabled) {
    form_set_error('macro', t('The following modules must be enabled for this import to work: %modules.', array(
        '%modules' => implode(', ', array_unique($not_enabled))
        )));
  }

  // Make sure the imported content type doesn't already exist in the database.
  if ($form_values['type_name'] == '<create>') {
    if (in_array($imported_type_name, array_keys($content_info['content types']))) {
      form_set_error('macro', t('The content type %type already exists in this database.', array(
            '%type' => $imported_type_name
            )));
    }
  }

  if (form_get_errors()) {
    drupal_set_message(t('Exiting. No import performed.'), 'error');
    return;
  }

  // Create the content type, if requested.
  if ($form_values['type_name'] == '<create>') {

    $type = (object) $imported_type;
    $values = $imported_type;
    // Prevent a warning in node/content_types.inc
    $type->has_title = TRUE;
    $type_form_state = array('values' => $values);

    // There's no API for creating node types, we still have to use drupal_execute().
    drupal_execute('node_type_form', $type_form_state, $type);

    // Reset type and database values once new type has been added.
    $type_name  = $imported_type_name;
    $type_label = node_get_types('name', $type_name);
    content_clear_type_cache();
    $content_info = _content_type_info();

    if (form_get_errors() || !isset($content_info['content types']) || !is_array($content_info['content types'][$type_name])) {
       drupal_set_message(t('An error has occurred adding the content type %type.<br/>Please check the errors displayed for more details.', array(
            '%type' => $imported_type_name
            )));
       return;
    }
  }

  // Create the groups for this type, if they don't already exist.
  if (module_exists('fieldgroup') && $imported_groups) {
    foreach ($imported_groups as $group) {
      $group_name = $group['group_name'];
      fieldgroup_save_group($type_name, $group);
    }
    // Reset the static variable in fieldgroup_groups() with new data.
    fieldgroup_groups('', FALSE, TRUE);
  }

  // Iterate through the field forms in the import and execute each.
  $rebuild = FALSE;
  foreach ($imported_fields as $field) {

    // Make sure the field doesn't already exist in the type.
    // If so, do nothing, fields can't be duplicated within a content type.
    $field_name   = $field['field_name'];

    // Might need to overwrite the content type name if a new type was created.
    $field['type_name'] = $type_name;

    if (!empty($field['field_name']) && isset($content_info['content types'][$type_name]['fields'][$field_name])) {
      drupal_set_message(t('The imported field %field_label (%field_name) was not added to %type because that field already exists in %type.', array(
        '%field_label' => $field['label'], '%field_name' => $field_name, '%type' => $type_label)));
    }
    else {
      $field = content_field_instance_create($field, FALSE);
      $rebuild = TRUE;
      drupal_set_message(t('The field %field_label (%field_name) was added to the content type %type.', array(
        '%field_label' => $field['widget']['label'], '%field_name' => $field_name, '%type' => $type_label)));
    }

    // Fieldgroup module erases all group related data when a module that
    // provides a content type is disabled, but CCK does not remove the fields.
    // In this case, we should ensure group data related to fields is properly
    // restored. Hence, we need to update field group data for newly imported
    // field, but also for fields that already exist.
    if (module_exists('fieldgroup') && isset($imported_groups)) {
      fieldgroup_update_fields($field);
    }
  }

  // Clear caches and rebuild menu only if any field has been created.
  if ($rebuild) {
    content_clear_type_cache(TRUE);
    menu_rebuild();
  }

  // Import weights of non-CCK fields.
  if (isset($content['extra'])) {
    variable_set('content_extra_weights_'. $type_name, $content['extra']);
  }
}

/**
 * Implementation of hook_form_alter().
 * Intervene to run form through macro when doing export
 */
function content_copy_form_alter(&$form, $form_state, $form_id) {
  $alter_forms = array('node_type_form', 'content_field_edit_form', 'fieldgroup_group_edit_form');
  if (isset($GLOBALS['content_copy']) && isset($GLOBALS['content_copy']['status']) && $GLOBALS['content_copy']['status'] == 'export' && in_array($form_id, $alter_forms)) {
    $form['#submit'][] = 'content_copy_record_macro';
  }
}

/**
 * Get all the *active* fields for a content type.
 */
function content_copy_fields($type_name) {
  $fields = array();
  if (!$type_name) {
    return $fields;
  }
  $content_info = _content_type_info();
  foreach ($content_info['content types'][$type_name]['fields'] as $field_name => $field) {
    // Omit fields from the export if their module is not currently installed
    // otherwise the system will generate errors when the macro tries to execute their forms.
    $field_types = _content_field_types();
    $field_module = $field_types[$field['type']]['module'];
    $widget_types = _content_widget_types();
    $widget_module = $widget_types[$field['widget']['type']]['module'];

    if (!$field['locked'] && !empty($field_module) && module_exists($field_module) && !empty($widget_module) && module_exists($widget_module)) {
      $fields[] = $field_name;
    }
  }
  return $fields;
}

/**
 *  Get all content types.
 */
function content_copy_types() {
  $types = array();
  $content_info = _content_type_info();
  foreach ($content_info['content types'] as $type_name => $val) {
    $types[$type_name] = check_plain($val['name']) .' ('. $type_name .')';
  }
  return $types;
}

/**
 * A handler that stores the form submissions into a $GLOBALS array
 */
function content_copy_record_macro($form, &$form_state) {
  $edit = $form_state['values'];
  $subs = isset($GLOBALS['content_copy']['submissions']) ? $GLOBALS['content_copy']['submissions'] : array();

  // Get the form values and store them in a $GLOBALS['content_copy']['submissions'] array.
  // Update $GLOBALS['content_copy']['count'] with an approximation of the number of rows in this item.
  // Count is used to approximate necessary size of textarea in form.

  $form_id = $form_state['values']['form_id'];
  if (isset($edit['type_name']) || isset($edit['submit']) || isset($edit['delete']) || isset($edit['form_id'])) {
    unset($edit['type_name'], $edit['submit'], $edit['delete'], $edit['form_id'], $edit['previous_field']);
  }
  switch ($form_id) {
    case 'node_type_form':
      $subs['type'] = $edit;
      $GLOBALS['content_copy']['count'] += sizeof($edit) + 5;
      break;

    case 'fieldgroup_group_edit_form':
      $subs['groups'][] = $edit;
      $GLOBALS['content_copy']['count'] += sizeof($edit) + 5;
      break;

    default:
      if (isset($edit['field_widget_type'])) {
        $tmp = explode('-', $edit['field_widget_type']);
        $field_name = $tmp[0];
      }
      else {
        $field_name = isset($edit['field_name']) ? $edit['field_name'] : '';
      }

      // The display settings are being fetched directly from the DB. During import,
      // we'll re-insert the data directly as well.
      //
      $query = 'SELECT display_settings FROM {'. content_instance_tablename() .'} WHERE field_name = \'%s\'';
      $row_info = db_fetch_array(db_query($query, $field_name));

      // If an error occurs, notify the user.
      if ($db_err = db_error()) {
        drupal_set_message(t("An error occurred when exporting the 'display settings' data for the field %field_name.<br/>The db error is: '%db_err'.", array(
             '%field_name' => $field_name,
             '%db_err' => $db_err
        )));
      }
      else {
        // The db fetch occurred successfully, unserialize the data blob and
        // insert it into a new "display_settings" field of the data.
        if ($display_settings = unserialize($row_info['display_settings'])) {
          $edit['display_settings'] = $display_settings;
        }
      }
      $subs['fields'][] = $edit;
      $GLOBALS['content_copy']['count'] += sizeof($edit) + 5;
      break;
  }

  $GLOBALS['content_copy']['submissions'] = $subs;
}

/**
 * @return a code representation of the recorded macro.
 */
function content_copy_get_macro() {
  // Define the indexes for the evaluated code.
  $string = "";
  if (array_key_exists('submissions', $GLOBALS['content_copy'])) {
    foreach ($GLOBALS['content_copy']['submissions'] as $form_type => $form) {
      $string .= "\$content['$form_type']  = ". var_export((array) $form, TRUE) .";\n";
    }
    return $string;
  }
}

function template_preprocess_content_copy_export_form($vars) {
  $form = &$vars['form'];

  if ($form['#step'] == 2) {
    $order = _content_overview_order($form, $form['#fields'], $form['#groups']);

    $rows = array();
    foreach ($order as $key) {
      $element = &$form[$key];
      $row = new stdClass();

      $row->row_type = $element['#row_type'];
      $checkbox_key = $element['#row_type'] == 'field' ? 'fields' : 'groups';
      $row->checkbox = drupal_render($form[$checkbox_key][$key]);
      foreach (element_children($element) as $child) {
        $row->{$child} = drupal_render($element[$child]);
      }
      $row->label_class = in_array($key, $form['#groups']) ? 'label-group' : 'label-field';
      $row->indentation = theme('indentation', isset($element['#depth']) ? $element['#depth'] : 0);

      $rows[] = $row;
    }
    $vars['rows'] = $rows;
  }

  $vars['submit'] = drupal_render($form);
}