> content >> migrate" * for analyzing data from various sources and importing them into Drupal tables. */ /** * Call a migrate hook */ function migrate_invoke_all($hook) { // Let modules do any one-time initialization (e.g., including migration support files) module_invoke_all('migrate_init'); $args = func_get_args(); $hookfunc = "migrate" . "_$hook"; unset($args[0]); $return = array(); $modulelist = module_implements($hookfunc); foreach ($modulelist as $module) { $function = $module . '_' . $hookfunc; $result = call_user_func_array($function, $args); if (isset($result) && is_array($result)) { $return = array_merge_recursive($return, $result); } elseif (isset($result)) { $return[] = $result; } } return $return; } /** * Save a new or updated content set * * @param $content_set * An array or object representing the content set. This is passed by reference (so * when adding a new content set the ID can be set) * @param $options * Array of additional options for saving the content set. Currently: * base_table: The base table of the view - if provided, we don't need * to load the view. * base_database: The database of the base table - if base_table is present * and base_database omitted, it defaults to 'default' * @return * The ID of the content set that was saved, or NULL if nothing was saved */ function migrate_save_content_set(&$content_set, $options = array()) { // Deal with objects internally (but remember if we need to put the parameter // back to an array) if (is_array($content_set)) { $was_array = TRUE; $content_set = (object) $content_set; } else { $was_array = FALSE; } // Update or insert the content set record as appropriate if (isset($content_set->mcsid)) { drupal_write_record('migrate_content_sets', $content_set, 'mcsid'); } else { drupal_write_record('migrate_content_sets', $content_set); } // Create or modify map and message tables $maptablename = _migrate_map_table_name($content_set->mcsid); $msgtablename = _migrate_message_table_name($content_set->mcsid); // TODO: For now, PK must be in base_table // If the caller tells us the base table of the view, we don't need // to load the view (which would not work when called from hook_install()) if (isset($options['base_table'])) { $tablename = $options['base_table']; if (isset($options['base_database'])) { $tabledb = $options['base_database']; } else { $tabledb = 'default'; } } else { // Get the proper field definition for the sourcekey $view = views_get_view($content_set->view_name); if (!$view) { drupal_set_message(t('View !view does not exist - either (re)create this view, or remove the content set using it.', array('!view' => $content_set->view_name))); return NULL; } // Must do this to load the database $view->init_query(); $tabledb = $view->base_database; $tablename = $view->base_table; } db_set_active($tabledb); $inspect = schema_invoke('inspect'); db_set_active('default'); $sourceschema = $inspect[$tablename]; // If the PK of the content set is defined, make sure we have a mapping table if ($content_set->sourcekey) { $sourcefield = $sourceschema['fields'][$content_set->sourcekey]; // The field name might be _... if (!$sourcefield) { $sourcekey = drupal_substr($content_set->sourcekey, drupal_strlen($tablename) + 1); $sourcefield = $sourceschema['fields'][$sourcekey]; } // But - we don't want serial fields to behave serially, so change to int if ($sourcefield['type'] == 'serial') { $sourcefield['type'] = 'int'; } $schema_change = FALSE; if (!db_table_exists($maptablename)) { $schema = _migrate_map_table_schema($sourcefield); db_create_table($ret, $maptablename, $schema); // Expose map table to views tw_add_tables(array($maptablename)); tw_add_fk($maptablename, 'destid'); $schema = _migrate_message_table_schema($sourcefield); db_create_table($ret, $msgtablename, $schema); // Expose messages table to views tw_add_tables(array($msgtablename)); tw_add_fk($msgtablename, 'sourceid'); $schema_change = TRUE; } else { // TODO: Deal with varchar->int case where there is existing non-int data $desired_schema = _migrate_map_table_schema($sourcefield); $actual_schema = $inspect[$maptablename]; if ($desired_schema['fields']['sourceid'] != $actual_schema['fields']['sourceid']) { $ret = array(); db_drop_primary_key($ret, $maptablename); db_change_field($ret, $maptablename, 'sourceid', 'sourceid', $sourcefield, array('primary key' => array('sourceid'))); tw_perform_analysis($maptablename); $schema_change = TRUE; } $desired_schema = _migrate_message_table_schema($sourcefield); $actual_schema = $inspect[$msgtablename]; if ($desired_schema['fields']['sourceid'] != $actual_schema['fields']['sourceid']) { $ret = array(); db_drop_index($ret, $msgtablename, 'sourceid'); db_change_field($ret, $msgtablename, 'sourceid', 'sourceid', $sourcefield, array('indexes' => array('sourceid' => array('sourceid')))); tw_perform_analysis($maptablename); $schema_change = TRUE; } } // Make sure the schema gets updated to reflect changes if ($schema_change) { cache_clear_all('schema', 'cache'); } } if ($was_array) { $content_set = (array)$content_set; return $content_set['mcsid']; } else { return $content_set->mcsid; } } function migrate_save_content_mapping(&$mapping) { if ($mapping->mcmid) { drupal_write_record('migrate_content_mappings', $mapping, 'mcmid'); } else { drupal_write_record('migrate_content_mappings', $mapping); } } function migrate_delete_content_set($mcsid) { // First, remove the map and message tables from the Table Wizard, and drop them $ret = array(); $maptable = _migrate_map_table_name($mcsid); $msgtable = _migrate_message_table_name($mcsid); if (db_table_exists($maptable)) { tw_remove_tables(array($maptable, $msgtable)); db_drop_table($ret, $maptable); db_drop_table($ret, $msgtable); } // Then, delete the content set data $sql = "DELETE FROM {migrate_content_mappings} WHERE mcsid=%d"; db_query($sql, $mcsid); $sql = "DELETE FROM {migrate_content_sets} WHERE mcsid=%d"; db_query($sql, $mcsid); } function migrate_delete_content_mapping($mcmid) { $sql = "DELETE FROM {migrate_content_mappings} WHERE mcmid=%d"; db_query($sql, $mcmid); } /** * Convenience function for generating a message array * * @param $message * Text describing the error condition * @param $type * One of the MIGRATE_MESSAGE constants, identifying the level of error * @return * Structured array suitable for return from an import hook */ function migrate_message($message, $type = MIGRATE_MESSAGE_ERROR) { $error = array( 'level' => $type, 'message' => $message, ); return $error; } /** * Add a mapping from source ID to destination ID for the specified content set * * @param $mcsid * ID of the content set being processed * @param $sourceid * Primary key value from the source * @param $destid * Primary key value from the destination */ function migrate_add_mapping($mcsid, $sourceid, $destid) { static $maptables = array(); if (!isset($maptables[$mcsid])) { $maptables[$mcsid] = _migrate_map_table_name($mcsid); } $mapping = new stdClass; $mapping->sourceid = $sourceid; $mapping->destid = $destid; drupal_write_record($maptables[$mcsid], $mapping); } /** * Clear migrated objects from the specified content set * * @param $mcsid * ID of the content set to clear * @param $messages * Array of messages to (ultimately) be displayed by the caller. * @param $options * Keyed array of optional options: * itemlimit - Maximum number of items to process * timelimit - Unix timestamp after which to stop processing * idlist - Comma-separated list of source IDs to process, instead of proceeding through * all unmigrated rows * feedback - Keyed array controlling status feedback to the caller * function - PHP function to call, passing a message to be displayed * frequency - How often to call the function * frequency_unit - How to interpret frequency (items or seconds) * * @return * Status of the migration process: */ function migrate_content_process_clear($mcsid, &$messages = array(), &$options = array()) { $itemlimit = $options['itemlimit']; $timelimit = $options['timelimit']; $idlist = $options['idlist']; $lastfeedback = time(); if (isset($options['feedback'])) { $feedback = $options['feedback']['function']; $frequency = $options['feedback']['frequency']; $frequency_unit = $options['feedback']['frequency_unit']; } $result = db_query("SELECT * FROM {migrate_content_sets} WHERE mcsid=%d", $mcsid); $tblinfo = db_fetch_object($result); $desttype = $tblinfo->desttype; $view_name = $tblinfo->view_name; $description = $tblinfo->description; $contenttype = $tblinfo->contenttype; $sourcekey = $tblinfo->sourcekey; $maptable = _migrate_map_table_name($mcsid); $msgtablename = _migrate_message_table_name($mcsid); $processstart = microtime(TRUE); $status = MIGRATE_STATUS_IN_PROGRESS; // If we're being called on a content set that isn't flagged for clearing, temporarily flag it $original_clearing = $tblinfo->clearing; if (!$original_clearing) { $sql = "UPDATE {migrate_content_sets} SET clearing=1 WHERE mcsid=%d"; db_query($sql, $mcsid); } $deleted = 0; if ($idlist) { $sql = "SELECT sourceid,destid FROM {" . $maptable . "} WHERE sourceid IN ($idlist)"; } else { $sql = "SELECT sourceid,destid FROM {" . $maptable . "}"; } timer_start('delete query'); if ($itemlimit) { $deletelist = db_query_range($sql, 0, $itemlimit); } else { $deletelist = db_query($sql); } timer_stop('delete query'); while ($row = db_fetch_object($deletelist)) { // Recheck clearing flag - permits dynamic interruption of jobs $sql = "SELECT clearing FROM {migrate_content_sets} WHERE mcsid=%d"; $clearing = db_result(db_query($sql, $mcsid)); if (!$clearing) { $status = MIGRATE_STATUS_CANCELLED; break; } // Check for time out if there is time info present if (isset($timelimit) && time() >= $timelimit) { $status = MIGRATE_STATUS_TIMEDOUT; break; } if (isset($feedback)) { if (($frequency_unit == 'seconds' && time()-$lastfeedback >= $frequency) || ($frequency_unit == 'items' && $deleted >= $frequency)) { $message = _migrate_progress_message($lastfeedback, $deleted, $description, FALSE, $status); $feedback($message); $lastfeedback = time(); $deleted = 0; } } // @TODO: Should return success/failure. Problem: node_delete doesn't return anything... migrate_invoke_all("delete_$contenttype", $row->destid); timer_start('clear map/msg'); db_query("DELETE FROM {" . $maptable . "} WHERE sourceid=%d", $row->sourceid); db_query("DELETE FROM {" . $msgtablename . "} WHERE sourceid=%d AND level=%d", $row->sourceid, MIGRATE_MESSAGE_INFORMATIONAL); timer_stop('clear map/msg'); $deleted++; } if ($status == MIGRATE_STATUS_IN_PROGRESS) { $status = MIGRATE_STATUS_SUCCESS; } $message = _migrate_progress_message($lastfeedback, $deleted, $description, FALSE, $status); if ($status == MIGRATE_STATUS_SUCCESS) { // Mark that we're done $tblinfo->clearing = 0; migrate_save_content_set($tblinfo); // Remove old messages before beginning new import process db_query("DELETE FROM {" . $msgtablename . "} WHERE level <> %d", MIGRATE_MESSAGE_INFORMATIONAL); } if (isset($feedback)) { $feedback($message); } else { $messages[] = $message; } watchdog('migrate', $message); if (!$original_clearing) { $sql = "UPDATE {migrate_content_sets} SET clearing=0 WHERE mcsid=%d"; db_query($sql, $mcsid); } return $status; } /** * Import objects from the specified content set * * @param $mcsid * ID of the content set to clear * @param $messages * Array of messages to (ultimately) be displayed by the caller. * @param $options * Keyed array of optional options: * itemlimit - Maximum number of items to process * timelimit - Unix timestamp after which to stop processing * idlist - Comma-separated list of source IDs to process, instead of proceeding through * all unmigrated rows * feedback - Keyed array controlling status feedback to the caller * function - PHP function to call, passing a message to be displayed * frequency - How often to call the function * frequency_unit - How to interpret frequency (items or seconds) * * @return * Status of the migration process: */ function migrate_content_process_import($mcsid, &$messages = array(), &$options = array()) { $itemlimit = $options['itemlimit']; $timelimit = $options['timelimit']; $idlist = $options['idlist']; $lastfeedback = time(); if (isset($options['feedback'])) { $feedback = $options['feedback']['function']; $frequency = $options['feedback']['frequency']; $frequency_unit = $options['feedback']['frequency_unit']; } $result = db_query("SELECT * FROM {migrate_content_sets} WHERE mcsid=%d", $mcsid); $tblinfo = db_fetch_object($result); $desttype = $tblinfo->desttype; $view_name = $tblinfo->view_name; $description = $tblinfo->description; $contenttype = $tblinfo->contenttype; $sourcekey = $tblinfo->sourcekey; $maptable = _migrate_map_table_name($mcsid); $msgtablename = _migrate_message_table_name($mcsid); $processstart = microtime(TRUE); $status = MIGRATE_STATUS_IN_PROGRESS; // If we're being called on a content set that isn't flagged for importing, temporarily flag it $original_importing = $tblinfo->importing || $tblinfo->scanning; if (!$original_importing) { $sql = "UPDATE {migrate_content_sets} SET importing=1 WHERE mcsid=%d"; db_query($sql, $mcsid); } $collist = db_query("SELECT srcfield, destfield, default_value FROM {migrate_content_mappings} WHERE mcsid=%d AND (srcfield <> '' OR default_value <> '') ORDER BY mcmid", $mcsid); $fields = array(); while ($row = db_fetch_object($collist)) { $fields[$row->destfield]['srcfield'] = $row->srcfield; $fields[$row->destfield]['default_value'] = $row->default_value; } $tblinfo->fields = $fields; $tblinfo->maptable = $maptable; // We pick up everything in the input view that is not already imported, and // not already errored out // Emulate views execute(), so we can scroll through the results ourselves $view = views_get_view($view_name); if (!$view) { $messages[] = t('View !view does not exist - either (re)create this view, or remove the content set using it.', array('!view' => $view_name)); return MIGRATE_STATUS_FAILURE; } $view->build(); // Let modules modify the view just prior to executing it. foreach (module_implements('views_pre_execute') as $module) { $function = $module . '_views_pre_execute'; $function($view); } $viewdb = $view->base_database; // Add a left join to the map table, and only include rows not in the map $join = new views_join; // Views prepends _ to column names other than the base table's // primary key - we need to strip that here for the join to work. But, it's // common for tables to have the tablename beginning field names (e.g., // table cms with PK cms_id). Deal with that as well... $baselen = drupal_strlen($view->base_table); if (!strncasecmp($sourcekey, $view->base_table . '_', $baselen + 1)) { // So, which case is it? Ask the schema module... db_set_active($viewdb); $inspect = schema_invoke('inspect', db_prefix_tables('{'. $view->base_table .'}')); db_set_active('default'); $tableschema = $inspect[$view->base_table]; $sourcefield = $tableschema['fields'][$sourcekey]; if (!$sourcefield) { $joinkey = drupal_substr($sourcekey, $baselen + 1); $sourcefield = $tableschema['fields'][$joinkey]; if (!$sourcefield) { $messages[] = t("In view !view, can't find key !key for table !table", array('!view' => $view_name, '!key' => $sourcekey, '!table' => $view->base_table)); return MIGRATE_STATUS_FAILURE; } } else { $joinkey = $sourcekey; } } else { $joinkey = $sourcekey; } $join->construct($maptable, $view->base_table, $joinkey, 'sourceid'); $view->query->add_relationship($maptable, $join, $view->base_table); $view->query->add_where(0, "$maptable.sourceid IS NULL", $view->base_table); // Ditto for the errors table $join = new views_join; $join->construct($msgtablename, $view->base_table, $joinkey, 'sourceid'); $view->query->add_relationship($msgtablename, $join, $view->base_table); $view->query->add_where(0, "$msgtablename.sourceid IS NULL", $view->base_table); // If running over a selected list of IDs, pass those in to the query if ($idlist) { $view->query->add_where($view->options['group'], $view->base_table . ".$sourcekey IN ($idlist)", $view->base_table); } // We can't seem to get $view->build() to rebuild build_info, so go straight into the query object $query = $view->query->query(); $query = db_rewrite_sql($query, $view->base_table, $view->base_field, array('view' => &$view)); $args = $view->build_info['query_args']; $replacements = module_invoke_all('views_query_substitutions', $view); $query = str_replace(array_keys($replacements), $replacements, $query); if (is_array($args)) { foreach ($args as $id => $arg) { $args[$id] = str_replace(array_keys($replacements), $replacements, $arg); } } // Now, make the current db name explicit if content set is pulling tables from another DB if ($viewdb <> 'default') { global $db_url; $url = parse_url(is_array($db_url) ? $db_url['default'] : $db_url); $currdb = drupal_substr($url['path'], 1); $query = str_replace('{' . $maptable . '}', $currdb . '.' . '{' . $maptable . '}', $query); $query = str_replace('{' . $msgtablename . '}', $currdb . '.' . '{' . $msgtablename . '}', $query); db_set_active($viewdb); } //drupal_set_message($query); timer_start('execute view query'); if ($itemlimit) { $importlist = db_query_range($query, $args, 0, $itemlimit); } else { $importlist = db_query($query, $args); } timer_stop('execute view query'); if ($viewdb != 'default') { db_set_active('default'); } $imported = 0; timer_start('db_fetch_object'); while ($row = db_fetch_object($importlist)) { timer_stop('db_fetch_object'); // Recheck importing flag - permits dynamic interruption of cron jobs $sql = "SELECT importing,scanning FROM {migrate_content_sets} WHERE mcsid=%d"; $checkrow = db_fetch_object(db_query($sql, $mcsid)); $importing = $checkrow->importing; $scanning = $checkrow->scanning; if (!($importing || $scanning)) { $status = MIGRATE_STATUS_CANCELLED; break; } // Check for time out if there is time info present if (isset($timelimit) && time() >= $timelimit) { $status = MIGRATE_STATUS_TIMEDOUT; break; } if (isset($feedback)) { if (($frequency_unit == 'seconds' && time()-$lastfeedback >= $frequency) || ($frequency_unit == 'items' && $imported >= $frequency)) { $message = _migrate_progress_message($lastfeedback, $imported, $description, TRUE, $status); $feedback($message); $lastfeedback = time(); $imported = 0; } } timer_start('import hooks'); $errors = migrate_invoke_all("import_$contenttype", $tblinfo, $row); timer_stop('import hooks'); // Ok, we're done. Preview the node or save it (if no errors). if (count($errors)) { $success = TRUE; foreach ($errors as $error) { if (!isset($error['level'])) { $error['level'] = MIGRATE_MESSAGE_ERROR; } if ($error['level'] != MIGRATE_MESSAGE_INFORMATIONAL) { $success = FALSE; } db_query("INSERT INTO {" . $msgtablename . "} (sourceid, level, message) VALUES('%s', %d, '%s')", $row->$sourcekey, $error['level'], $error['message']); } if ($success) { $imported++; } } else { $imported++; } timer_start('db_fetch_object'); } timer_stop('db_fetch_object'); if ($status == MIGRATE_STATUS_IN_PROGRESS) { $status = MIGRATE_STATUS_SUCCESS; } $message = _migrate_progress_message($lastfeedback, $imported, $description, TRUE, $status); if ($status == MIGRATE_STATUS_SUCCESS) { // Remember we're done if ($importing) { db_query("UPDATE {migrate_content_sets} SET importing=0, lastimported=NOW() WHERE mcsid=%d", $mcsid); } else { db_query("UPDATE {migrate_content_sets} SET lastimported=NOW() WHERE mcsid=%d", $mcsid); } } if (isset($feedback)) { $feedback($message); } else { $messages[] = $message; } watchdog('migrate', $message); if (!$original_importing) { $sql = "UPDATE {migrate_content_sets} SET importing=0 WHERE mcsid=%d"; db_query($sql, $mcsid); } return $status; } /* Revisit function migrate_content_process_all_action(&$dummy, $action_context, $a1, $a2) { migrate_content_process_all(time()); } */ function migrate_content_process_all_batch($starttime, $limit, $idlist, &$context) { $messages = array(); // A zero max_execution_time means no limit - but let's set a reasonable // limit anyway $starttime = time(); $maxexectime = ini_get('max_execution_time'); if (!$maxexectime) { $maxexectime = 240; } // Initialize the Batch API context $context['finished'] = 0; // The Batch API progress bar will reflect the number of operations being // done (clearing/importing/scanning) if (!isset($context['sandbox']['numops'])) { $numops = 0; $sql = "SELECT COUNT(*) FROM {migrate_content_sets} WHERE clearing=1"; $numops = db_result(db_query($sql)); $sql = "SELECT COUNT(*) FROM {migrate_content_sets} WHERE importing=1 OR scanning=1"; $numops += db_result(db_query($sql)); $context['sandbox']['numops'] = $numops; $context['sandbox']['numopsdone'] = 0; } // For the timelimit, subtract more than enough time to clean up $options = array( 'itemlimit' => $limit, 'timelimit' => $starttime + (($maxexectime < 20) ? $maxexectime : ($maxexectime - 20)), 'idlist' => $idlist, ); $status = migrate_content_process_all($messages, $options); foreach ($messages as $message) { $context['sandbox']['message'] .= $message . '
'; $context['message'] = $context['sandbox']['message']; $context['results'][] = $message; $context['sandbox']['numopsdone'] += $options['opcount']; } // If we did not arrive via a timeout, we must have finished all operations if ($status != MIGRATE_STATUS_TIMEDOUT) { $context['finished'] = 1; } else { // Not done, report what percentage done we are (in terms of number of operations) $context['finished'] = $context['sandbox']['numopsdone']/$context['sandbox']['numops']; } // If requested save timers for eventual display if (variable_get('migrate_display_timers', 0)) { global $timers; foreach ($timers as $name => $timerec) { if (isset($timerec['time'])) { $context['sandbox']['times'][$name] += $timerec['time']/1000; } } // When all done, display the timers if ($context['finished'] == 1 && isset($context['sandbox']['times'])) { global $timers; arsort($context['sandbox']['times']); foreach ($context['sandbox']['times'] as $name => $total) { drupal_set_message("$name: " . round($total, 2)); } } } } /** * Process all enabled migration processes * * @param $messages * Array of messages to (ultimately) be displayed by the caller. * @param $options * Keyed array of optional options: * itemlimit - Maximum number of items to process * timelimit - Unix timestamp after which to stop processing * idlist - Comma-separated list of source IDs to process, instead of proceeding through * all unmigrated rows * opcount - Number of clearing or import operations performed * feedback - Keyed array controlling status feedback to the caller * function - PHP function to call, passing a message to be displayed * frequency - How often to call the function * frequency_unit - How to interpret frequency (items or seconds) * * @return * Status of the migration process: */ function migrate_content_process_all(&$messages = array(), &$options = array()) { if (variable_get('migrate_semaphore', FALSE)) { drupal_set_message('There is an import process already in progress'); return 0; } variable_set('migrate_semaphore', TRUE); // First, perform any clearing actions in reverse order $result = db_query("SELECT mcsid FROM {migrate_content_sets} WHERE clearing=1 ORDER BY weight DESC"); $context['sandbox']['timedout'] = FALSE; while ($row = db_fetch_object($result)) { $status = migrate_content_process_clear($row->mcsid, $messages, $options); if ($status != MIGRATE_STATUS_SUCCESS) { break; } $options['opcount']++; } // Then, any import actions going forward $result = db_query("SELECT mcsid FROM {migrate_content_sets} WHERE importing=1 OR scanning=1 ORDER BY weight"); while ($row = db_fetch_object($result)) { $status = migrate_content_process_import($row->mcsid, $messages, $options); if ($status != MIGRATE_STATUS_SUCCESS) { break; } $options['opcount']++; } variable_del('migrate_semaphore'); return $status; } function _migrate_progress_message($starttime, $numitems, $description, $import = TRUE, $status = MIGRATE_STATUS_SUCCESS) { $time = (microtime(TRUE) - $starttime); if ($time > 0) { $perminute = round(60*$numitems/$time); $time = round($time, 1); } else { $perminute = '?'; } if ($import) { switch ($status) { case MIGRATE_STATUS_SUCCESS: $basetext = "!numitems items imported in !time seconds (!perminute/min) - done importing '!description'";; break; case MIGRATE_STATUS_FAILURE: $basetext = "!numitems items imported in !time seconds (!perminute/min) - failure importing '!description'";; break; case MIGRATE_STATUS_TIMEDOUT: case MIGRATE_STATUS_IN_PROGRESS: $basetext = "!numitems items imported in !time seconds (!perminute/min) - continuing importing '!description'"; break; case MIGRATE_STATUS_CANCELLED: $basetext = "!numitems items imported in !time seconds (!perminute/min) - cancelled importing '!description'"; break; } } else { switch ($status) { case MIGRATE_STATUS_SUCCESS: $basetext = "!numitems previously-imported items deleted in !time seconds (!perminute/min) - done clearing '!description'";; break; case MIGRATE_STATUS_FAILURE: $basetext = "!numitems previously-imported items deleted in !time seconds (!perminute/min) - failure clearing '!description'";; break; case MIGRATE_STATUS_TIMEDOUT: case MIGRATE_STATUS_IN_PROGRESS: $basetext = "!numitems previously-imported items deleted in !time seconds (!perminute/min) - continuing clearing '!description'"; break; case MIGRATE_STATUS_CANCELLED: $basetext = "!numitems previously-imported items deleted in !time seconds (!perminute/min) - cancelled clearing '!description'"; break; } } $message = t($basetext, array('!numitems' => $numitems, '!time' => $time, '!perminute' => $perminute, '!description' => $description)); return $message; } /* * Implementation of hook_init(). */ function migrate_init() { // Loads the hooks for the supported modules. // TODO: Be more lazy - only load when really needed $path = drupal_get_path('module', 'migrate') .'/modules'; $files = drupal_system_listing('.*\.inc$', $path, 'name', 0); foreach ($files as $module_name => $file) { if (module_exists($module_name)) { include_once($file->filename); } } // Add main CSS functionality. drupal_add_css(drupal_get_path('module', 'migrate') .'/migrate.css'); } /** * Implementation of hook_action_info(). */ /* Revisit function migrate_action_info() { $info['migrate_content_process_clear'] = array( 'type' => 'migrate', 'description' => t('Clear a migration content set'), 'configurable' => FALSE, 'hooks' => array( 'cron' => array('run'), ), ); $info['migrate_content_process_import'] = array( 'type' => 'migrate', 'description' => t('Import a migration content set'), 'configurable' => FALSE, 'hooks' => array( 'cron' => array('run'), ), ); $info['migrate_content_process_all_action'] = array( 'type' => 'migrate', 'description' => t('Perform all active migration processes'), 'configurable' => FALSE, 'hooks' => array( 'cron' => array('run'), ), ); return $info; } */ /** * Implementation of hook_cron(). * */ function migrate_cron() { $path = drupal_get_path('module', 'migrate') . '/migrate_pages.inc'; include_once($path); // Elevate privileges so node deletion/creation works in cron session_save_session(FALSE); global $user; $saveuser = $user; $user = user_load(array('uid' => 1)); $messages = array(); // A zero max_execution_time means no limit - but let's set a reasonable // limit anyway $starttime = variable_get('cron_semaphore', 0); $maxexectime = ini_get('max_execution_time'); if (!$maxexectime) { $maxexectime = 240; } $options = array('timelimit' => $starttime + (($maxexectime < 20) ? $maxexectime : ($maxexectime - 20))); migrate_content_process_all($messages, $options); $user = $saveuser; session_save_session(TRUE); } /** * Implementation of hook_perm(). */ function migrate_perm() { return array(MIGRATE_ACCESS_BASIC, MIGRATE_ACCESS_ADVANCED); } /** * Implementation of hook_help(). */ function migrate_help($page, $arg) { switch ($page) { case 'admin/content/migrate': return theme('advanced_help_topic', 'migrate', 'about', 'icon') . t('Click the question marks like this one to read the migrate module help topics.'); case 'admin/content/migrate/content_sets': return t('Define sets of mappings from imported tables to Drupal content. These are the migrations which are later processed.'); case 'admin/content/migrate/process': return t('View and manage import processes here. Processes that are in progress are checked - they can be cancelled by unchecking, or new processes begun by checking, then clicking Submit. Any checked process will run in the background (via cron) automatically - you may also run them interactively.'); case 'admin/content/migrate/tools': return t('Besides content that is migrated into a new site, nodes may be manually created during the testing process. Typically you will want to clear these before the final migration - if you are absolutely positive that all nodes of a given type should be deleted, you may do so here.'); } } /** * Implementation of hook_menu(). */ function migrate_menu() { $items = array(); $items['admin/content/migrate'] = array( 'title' => 'Migrate', 'description' => 'Manage data migration from external sources', 'page callback' => 'migrate_front', 'access arguments' => array(MIGRATE_ACCESS_BASIC), 'file' => 'migrate_pages.inc', ); $items['admin/content/migrate/content_sets'] = array( 'title' => 'Content sets', 'description' => 'Manage content sets: mappings of source data to Drupal content', 'weight' => 2, 'page callback' => 'migrate_content_sets', 'access arguments' => array(MIGRATE_ACCESS_ADVANCED), 'file' => 'migrate_pages.inc', ); $items['admin/content/migrate/process'] = array( 'title' => 'Process', 'description' => 'Perform and monitor the creation of Drupal content from source data', 'weight' => 3, 'page callback' => 'migrate_dashboard', 'access arguments' => array(MIGRATE_ACCESS_BASIC), 'file' => 'migrate_pages.inc', ); $items['admin/content/migrate/tools'] = array( 'title' => 'Tools', 'description' => 'Additional tools for managing migration', 'weight' => 4, 'page callback' => 'migrate_tools', 'access arguments' => array(MIGRATE_ACCESS_ADVANCED), 'file' => 'migrate_pages.inc', ); $items['admin/content/migrate/settings'] = array( 'title' => 'Settings', 'description' => 'Migrate module settings', 'weight' => 5, 'page callback' => 'migrate_settings', 'access arguments' => array(MIGRATE_ACCESS_ADVANCED), 'file' => 'migrate_pages.inc', ); $items['admin/content/migrate/content_sets/%'] = array( 'title' => 'Content set', 'page callback' => 'drupal_get_form', 'page arguments' => array('migrate_content_set_mappings', 4), 'access arguments' => array(MIGRATE_ACCESS_ADVANCED), 'type' => MENU_CALLBACK, 'file' => 'migrate_pages.inc', ); $items['migrate/xlat/%'] = array( 'page callback' => 'migrate_xlat', 'access arguments' => array('access content'), 'page arguments' => array(2), 'type' => MENU_CALLBACK, ); return $items; } /** * Implementation of hook_schema_alter(). * @param $schema */ function migrate_schema_alter(&$schema) { // Check for table existence - at install time, hook_schema_alter() may be called // before our install hook. if (db_table_exists('migrate_content_sets')) { $result = db_query("SELECT * FROM {migrate_content_sets}"); while ($content_set = db_fetch_object($result)) { $maptablename = _migrate_map_table_name($content_set->mcsid); $msgtablename = _migrate_message_table_name($content_set->mcsid); // Get the proper field definition for the sourcekey $view = views_get_view($content_set->view_name); if (!$view) { drupal_set_message(t('View !view does not exist - either (re)create this view, or remove the migrate content set using it.', array('!view' => $content_set->view_name))); continue; } // Must do this to load the database $view->init_query(); // TODO: For now, PK must be in base_table $tabledb = $view->base_database; $tablename = $view->base_table; db_set_active($tabledb); $inspect = schema_invoke('inspect'); db_set_active('default'); $sourceschema = $inspect[$tablename]; // If the PK of the content set is defined, make sure we have a mapping table if ($sourcekey = $content_set->sourcekey) { $sourcefield = $sourceschema['fields'][$sourcekey]; if (!$sourcefield) { // strip base table name if views prepended it $baselen = drupal_strlen($tablename); if (!strncasecmp($sourcekey, $tablename . '_', $baselen + 1)) { $sourcekey = drupal_substr($sourcekey, $baselen + 1); } $sourcefield = $sourceschema['fields'][$sourcekey]; } // We don't want serial fields to behave serially, so change to int if ($sourcefield['type'] == 'serial') { $sourcefield['type'] = 'int'; } $schema[$maptablename] = _migrate_map_table_schema($sourcefield); $schema[$msgtablename] = _migrate_message_table_schema($sourcefield); } } } } /* * Translate URIs from an old site to the new one * Requires adding RewriteRules to .htaccess. For example, if the URLs * for news articles had the form * http://example.com/issues/news/[OldID].html, use this rule: * * RewriteRule ^issues/news/([0-9]+).html$ /migrate/xlat/node/$1 [L] */ function migrate_xlat($contenttype=NULL, $oldid=NULL) { $uri = ''; if ($contenttype && $oldid) { $newid = _migrate_xlat_get_new_id($contenttype, $oldid); if ($newid) { $uri = migrate_invoke_all("xlat_$contenttype", $newid); drupal_goto($uri[0], NULL, NULL, 301); } } } /* * Helper function to translate an ID from a source file to the corresponding * Drupal-side ID (nid, uid, etc.) * * TODO: Update to new world (content sets as the basis of migration) */ function _migrate_xlat_get_new_id($contenttype, $oldid) { $result = db_query("SELECT DISTINCT mf.importtable, mc.colname FROM {migrate_content_sets} mcs INNER JOIN {migrate_files} mf ON mcs.mfid=mf.mfid INNER JOIN {migrate_columns} mc ON mf.mfid=mc.mfid AND chosenpk=1 WHERE mcs.contenttype='%s'", $contenttype); while ($row = db_fetch_object($result)) { $table = _migrate_map_table_name($row->mcsid); $pkcol = $row->colname; $id = db_result(db_query("SELECT destid FROM {$table} WHERE sourceid=%d", $oldid)); if ($id) { return $id; } } return NULL; } /** * Implementation of hook_theme(). * * Registers all theme functions used in this module. */ function migrate_theme() { return array( 'migrate_mapping_table' => array('arguments' => array('form')), '_migrate_dashboard_form' => array( 'arguments' => array('form' => NULL), 'function' => 'theme_migrate_dashboard', ), '_migrate_tools_form' => array( 'arguments' => array('form' => NULL), 'function' => 'theme_migrate_tools', ), '_migrate_settings_form' => array( 'arguments' => array('form' => NULL), 'function' => 'theme_migrate_settings', ), 'migrate_content_set_mappings' => array( 'arguments' => array('form' => NULL), 'function' => 'theme_migrate_content_set_mappings', ), ); } function _migrate_map_table_name($mcsid) { return "migrate_map_$mcsid"; } function _migrate_message_table_name($mcsid) { return "migrate_msgs_$mcsid"; } function _migrate_map_table_schema($sourcefield) { $schema = array( 'description' => t('Mappings from source key to destination key'), 'fields' => array( 'sourceid' => $sourcefield, // @TODO: Assumes destination key is unsigned int 'destid' => array( 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, ), ), 'primary key' => array('sourceid'), 'indexes' => array( 'idkey' => array('destid'), ), ); return $schema; } function _migrate_message_table_schema($sourcefield) { $schema = array( 'description' => t('Import errors'), 'fields' => array( 'mceid' => array( 'type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE, ), 'sourceid' => $sourcefield, 'level' => array( 'type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 1, ), 'message' => array( 'type' => 'text', 'size' => 'medium', 'not null' => TRUE, ), ), 'primary key' => array('mceid'), 'indexes' => array( 'sourceid' => array('sourceid'), ), ); return $schema; } function migrate_views_api() { return array('api' => '2.0'); } /** * Check to see if the advanced help module is installed, and if not put up * a message. * * Only call this function if the user is already in a position for this to * be useful. */ function migrate_check_advanced_help() { if (variable_get('migrate_hide_help_message', FALSE)) { return; } if (!module_exists('advanced_help')) { $filename = db_result(db_query("SELECT filename FROM {system} WHERE type = 'module' AND name = 'advanced_help'")); if ($filename && file_exists($filename)) { drupal_set_message(t('If you enable the advanced help module, Migrate will provide more and better help. Hide this message.', array('@modules' => url('admin/build/modules'), '@hide' => url('admin/build/views/tools')))); } else { drupal_set_message(t('If you install the advanced help module from !href, Migrate will provide more and better help. Hide this message.', array('!href' => l('http://drupal.org/project/advanced_help', 'http://drupal.org/project/advanced_help'), '@hide' => url('admin/content/migrate/settings')))); } } } /** * Check if a date is valid and return the correct * timestamp to use. Returns -1 if the date is not * considered valid. */ function _migrate_valid_date($date) { //TODO: really check whether the date is valid!! if (empty($date)) { return -1; } if (is_numeric($date) && $date > -1) { return $date; } // strtotime() doesn't recognize dashes as separators, change to slashes $date = str_replace('-', '/', $date); $time = strtotime($date); if ($time < 0 || !$time) { // Handles form YYYY-MM-DD HH:MM:SS.garbage if (drupal_strlen($date) > 19) { $time = strtotime(drupal_substr($date, 0, 19)); if ($time < 0 || !$time) { return -1; } } } return $time; }