diff --git a/phone.info b/phone.info index 95e3d4c..e4994f2 100644 --- a/phone.info +++ b/phone.info @@ -4,5 +4,6 @@ package = Fields dependencies[] = field dependencies[] = libraries (>=2.x) files[] = phone.migrate.inc +files[] = phone.content_migrate.inc core = 7.x php = 5.3 diff --git a/phone.install b/phone.install index 36bdf43..b6ec58a 100644 --- a/phone.install +++ b/phone.install @@ -77,3 +77,256 @@ function phone_field_schema($field) { return $schema; } + +/** + * The phone field schema specifically as defined at the time when + * phone_update_7200() was written. Any future updates to the + * schema need to be specified in a separate function without + * altering this function's contents. + */ +function _phone_field_schema_7200() { + $columns = array( + // Normally 'number' is the national phone number, and contains nothing but + // digits. For this purpose, the maximum length would be 14 (international + // standard states that the full number -- country code + national number -- + // is at most 15 digits. The shortest country code is 1 digit. + // + // However, in cases where there are errors processing a phone number, + // 'number' may contain the country code + national number, and in this + // case the user-provided formatting (spaces, punctuation, parentheses) + // is left in place. This is a contingency to handle errors that occur + // during various types of imports, and also for cases where the + // libphonenumber library is not available. To allow this information to + // be stored in the database without truncation, the database field length + // is 30 instead of 14. + 'number' => array( + 'description' => 'The national phone number, i.e., the phone number without country code or extension', + 'type' => 'varchar', + 'length' => 30, + 'not null' => FALSE, + ), + // In cases where there were errors processing the phone number, the + // country_code is null. + 'country_code' => array( + 'description' => 'The two-letter country code for the phone number', + 'type' => 'char', + 'length' => 2, + 'not null' => FALSE, + ), + // This is the extension, without any prefix ('x', 'ext.', etc.). + // libphonenumber allows extensions of up to 7 digits. + 'extension' => array( + 'description' => 'The phone number extension (optional)', + 'type' => 'varchar', + 'length' => 7, + 'not null' => FALSE, + ), + // The phone type, using the hcard defined values for type + // (http://microformats.org/wiki/hcard). Note that hcard allows the type + // to be multivalued, so this field has been set to be large enough to + // allow up to three comma-separated types (e.g., "Work,Voice,Pref"). + // However, support for multiple values has not otherwise been incorporated + // at this time. + 'type' => array( + 'description' => 'The phone number type (e.g., Home, Work, Cell)', + 'type' => 'varchar', + 'length' => 20, + 'not null' => FALSE, + ), + ); + return array( + 'columns' => $columns, + // Add indexes for country_code and type to make filtering/sorting + // more efficient. + 'indexes' => array( + 'country_code' => array('country_code'), + 'type' => array('type'), + ), + ); +} + +/** + * Implements hook_update_N(). + * + * Update from phone-7.x-1.x to phone-7.x-2.x, including database changes and + * migrating field instance settings. + */ +// Notes: +// Updates from phone-6.x and cck_phone-6.x to phone-7.x-2.x are handled +// by the content_migrate module that is used to migrate CCK fields, via +// the phone_content_migrate_* functions in phone.module. +// phone-7.x-1.x did not include any updates -- otherwise +// hook_update_last_removed would probably need to be used to document +// that they've been dropped from this branch. +function phone_update_7200(&$sandbox) { + // First loop of batch processing: update overall information including + // table schemas and instance settings. + if (!isset($sandbox['phone_instances'])) { + $sandbox['phone_instances'] = array(); + $sandbox['phone_tables'] = array(); + $sandbox['progress'] = 0; + $sandbox['max'] = 0; + $sandbox['#finished'] = 0; + + // Get the 7200 version of the schema. + // This needs to be the 7200-specific schema so that updates can be + // done in sequence correctly, even if there are future schema changes. + // (see http://drupal.org/node/150220). + $schema = _phone_field_schema_7200(); + + // Get list of phone field instances directly from field_config_instance + // database table, in order to be sure to get now-obsolete instance + // settings. This is also more efficient than using field_info_instances(), + // since field_info_instances() does not provide a way to directly limit the + // list based on field type. + $query = db_select('field_config', 'fc') + ->condition('fc.type', 'phone'); + $query->addField('fc', 'field_name', 'fc_field_name'); + $query->addField('fc', 'data', 'fc_data'); + $query->leftJoin('field_config_instance', 'fci', 'fc.id = fci.field_id'); + $query->fields('fci'); + $results = $query->execute(); + + $tables_done = array(); + $field_country = array(); + foreach ($results as $row) { + $field_name = $row->fc_field_name; + + // Update the schema of this instance's database table (if it has not yet + // been done). + // Note: although built-in functions such as field_udpate_field() and + // field_update_instance() exist, they explicitly don't allow schema + // changes. So stick to manually changing the information that needs + // be changed. + if (empty($tables_done[$field_name])) { + $tables_done[$field_name] = TRUE; + $data = unserialize($row->fc_data); + + // Get country setting -- being moved from field_config level to + // instance level + $country = _phone_update_phone_country($data['settings']['country']); + $field_country[$field_name] = array('orig' => $data['settings']['country'], 'new' => $country); + + foreach (array('data', 'revision') as $table_type) { + $table_name = 'field_' . $table_type . '_' . $field_name; + if (!db_table_exists($table_name)) + continue; + $nrows = db_select($table_name) + ->countQuery() + ->execute() + ->fetchField(); + $sandbox['phone_tables'][$table_name] = array( + 'field_name' => $field_name, + 'nrows' => $nrows, + 'country' => $field_country[$field_name], + ); + $sandbox['max'] += $nrows; + // Add all new schema columns. + // Don't want to delete the value column until after data has been + // transferred. + foreach ($schema['columns'] as $column => $coldata) { + // Note that the column name is the field-version of the column; + // need to prepend $field_name to get the database column name. + db_add_field($table_name, $field_name . '_' . $column, $coldata); + } + // Do not add indexes manually, because field_update_field() + // automatically manages the indexes, causing 'index already + // exists' errors later. + } + + // Call field_update_field to force indexes to be updated, along with + // internal information about indexes. + // Note that this does NOT update the rest of the lists of fields under + // FIELD_LOAD_CURRENT and FIELD_LOAD_REVISION, but there's no apparent + // way to do that, nor is it done by any schema update examples. + field_update_field(array('field_name' => $field_name)); + } + + // Skip instance processing if a field does not have any associated instances. + if (empty($row->data)) { + continue; + } + + $data = unserialize($row->data); + + _phone_update_phone_instance_settings($data, $field_country[$field_name]); + db_update('field_config_instance') + ->fields(array('data' => serialize($data))) + ->condition('id', $row->id) + ->execute(); + } + // Clear the field cache. + field_cache_clear(); + } + elseif (empty($sandbox['phone_tables'])) { + $sandbox['#finished'] = 1; + $sandbox['progress'] = $sandbox['max']; + } + // Update the contents of the field_data and field_revision tables. This is + // done using batch-mode processing in case there are a lot of entries. + else { + $phone_table = array_shift(array_keys($sandbox['phone_tables'])); + $table_data = $sandbox['phone_tables'][$phone_table]; + $field_name = $table_data['field_name']; + $process_limit = 20; + + // Get next set of rows to process. + // Only condition is that the number is null, because all non-processed + // rows have null numbers. + $query = db_select($phone_table) + ->fields($phone_table) + ->isNull($table_data['field_name'] . '_number') + ->range(0, $process_limit); + $result = $query->execute(); + $nrows_done = 0; + foreach ($result as $row) { + $nrows_done++; + + // Convert number to individual entries + $newvals = _phone_migrate_phone_number($row->{$field_name . '_value'}, $table_data['country']['new']); + + // Prefix $field_name onto each field value's name + foreach ($newvals as $valname => $value) { + $newvals[$field_name . '_' . $valname] = $value; + unset($newvals[$valname]); + } + if (empty($newvals[$field_name . '_number'])) { + // Make sure to set number to a non-NULL value, because a NULL value + // implies that the row has not been processed -- which will cause an + // infinite batch processing loop. + $newvals[$field_name . '_number'] = ''; + } + + db_update($phone_table) + ->fields($newvals) + ->condition('entity_type', $row->entity_type) + ->condition('entity_id', $row->entity_id) + ->condition('revision_id', $row->revision_id) + ->condition('delta', $row->delta) + ->condition('language', $row->language) + ->execute(); + } + + // If done processing this table, drop it from the sandbox and + // delete the value column. + // We're done if the query returned less than the maximum number of rows. + // Note that in the case where there were exactly $process_limit rows left + // to process, the table won't be tagged as done until the next loop, at + // which point the query will return 0 rows. + if ($nrows_done<$process_limit) { + unset($sandbox['phone_tables'][$phone_table]); + db_drop_field($phone_table, $field_name . '_value'); + } + + $sandbox['progress'] += $nrows_done; + if (empty($sandbox['phone_tables'])) { + $sandbox['#finished'] = 1; + } + else { + // We're not directly using progress/max to determine when processing + // is done, so don't allow this value to reach 1 until phone_tables has + // been emptied (otherwise, the final table cleanup could get skipped). + $sandbox['#finished'] = min(0.99, $sandbox['progress']/$sandbox['max']); + } + } +} \ No newline at end of file diff --git a/phone.module b/phone.module index 00dc3ff..cb43184 100644 --- a/phone.module +++ b/phone.module @@ -1174,3 +1174,249 @@ function phone_process_field(&$variables) { } } } + +function _phone_convert_phone_item_to_object($item, $settings, $reparse=FALSE, &$error=NULL) { + $phone_util = phone_load_libphonenumber(); + if (empty($item['number'])) { + return NULL; + } + + // If item contains an already-parsed number, just copy the information to + // the phone object. + if (!empty($item['country_code']) && !$reparse) { + $country_info = phone_countrycodes($item['country_code']); + $phone_obj = new com\google\i18n\phonenumbers\PhoneNumber(); + $phone_obj->setCountryCode($country_info['number']); + $phone_obj->setNationalNumber($item['number']); + $phone_obj->setCountryCodeSource($settings['default_country']); + } + else { + if (!empty($item['country_code'])) { + $country_code = $item['country_code']; + } + elseif (!empty($settings['default_country'])) { + $country_code = $settings['default_country']; + } + else { + $country_code = variable_get('site_default_country', 'US'); + } + $country_code = drupal_strtoupper($country_code); + + try { + $phone_obj = $phone_util->parseAndKeepRawInput($item['number'], $country_code); + } + catch (com\google\i18n\phonenumbers\NumberParseException $e) { + $error = $e; + return NULL; + } + } + + if (!empty($item['extension'])) { + $phone_obj->setExtension($item['extension']); + } + return $phone_obj; +} + +/** + * Helper function for migrate-type functions: convert a number from a + * single-entry number to country code, number, and extension. + * + * The main difference between the processing here and in + * phone_element_validate() is that "bad" data is allowed through, + * without any processing. + */ +function _phone_migrate_phone_number($number, $default_country=NULL) { + // @todo: create a version of the processing that does not rely on + // libphonenumber. + $phone_util = phone_load_libphonenumber(); + + // Can't process an empty number + if (empty($number)) { + return array(); + } + + $temp_item = array('number' => $number); + $processed_phone = _phone_convert_phone_item_to_object($temp_item, array('default_country' => $default_country), TRUE); + + // If there was any type of error processing the old phone number, + // simply stuff the existing value into number so that information is + // not lost. + if (empty($processed_phone)) { + return array('number' => $number); + } + + $item = array(); + // Not doing any validation checks here, because there's no way to get + // user to fix the problem, and it's better to keep invalid info than + // discard it. + $country_code = $phone_util->getRegionCodeForNumber($processed_phone); + if (!empty($country_code)) { + $item['country_code'] = $country_code; + $item['number'] = $processed_phone->getNationalNumber(); + if ($processed_phone->getExtension()) { + $item['extension'] = $processed_phone->getExtension(); + } + } + else { + // If country code could not be determined, treat as another error. + $item['number'] = $number; + } + + return $item; +} + +/** + * Helper function for migrate/update functions: convert phone-6.x and + * phone-7.x-1.x country specification to new value. + */ +function _phone_update_phone_country($orig_country) { + if ($orig_country=='int') { + // If 'internaional' was used, set new default country based on + // site's country. + return variable_get('site_default_country', 'US'); + } + else { + $country = drupal_strtoupper($orig_country); + // Fix the country code as necessary. + switch ($country) { + // Change 'ca' to 'us' because it's far more likely that's what the + // user wanted. + case 'CA': + $country = 'US'; + break; + // Greece is GR, not EL + case 'EL': + $country = 'GR'; + break; + // Czech Republic is CZ, not CS + case 'CS': + $country = 'CZ'; + break; + } + return $country; + } +} + +/** + * Helper function for migrate/update functions: convert phone-6.x and + * phone-7.x-1.x instance settings to new values. + */ +function _phone_update_phone_instance_settings(&$data, $country_info) { + // Get our new field info, in particular the default instance settings. + $info = phone_field_info(); + $format_info = phone_field_formatter_info(); + + // Initialize all missing/changed instance information. + $data['widget_type'] = $info['phone']['default_widget']; + + $data['settings'] += $info['phone']['instance_settings']; + $data['settings']['default_country'] = $country_info['new']; + if ($country_info['orig']=='int') { + $data['settings']['enable_default_country'] = FALSE; + $data['settings']['all_country_codes'] = TRUE; + } + else { + $data['settings']['enable_default_country'] = TRUE; + $data['settings']['all_country_codes'] = FALSE; + $data['settings']['country_codes']['hide_single_cc'] = TRUE; + $data['settings']['country_codes']['country_selection'] = array($country_info['new'] => 1); + } + // Update all display formatters to be as similar as possible to + // previous formats. + foreach ($data['display'] as $context => $ddata) { + if (is_array($ddata)) { + if ($country_info['orig']=='int') { + $data['display'][$context]['type'] = 'phone_global'; + } + else { + $data['display'][$context]['type'] = 'phone_local'; + } + $data['display'][$context]['settings'] = $format_info[$data[$context]['type']]['settings']; + $data['display'][$context]['settings']['country_name_position'] = 'none'; + } + } + + // Clear obsolete instance settings. + unset($data['settings']['phone_country_code']); + unset($data['settings']['phone_default_country_code']); + unset($data['settings']['phone_int_max_length']); + // @todo: Put formatting information into formatter. But what values were people using? + // Not all options can be transferred over, but should the options that correspond to + // new settings be transferred (such as empty values for both)? + unset($data['settings']['ca_phone_separator']); + unset($data['settings']['ca_phone_parentheses']); +} + +/** + * Helper function for migrate/update functions: convert cck_phone-6.x and + * cck_phone-7.x-1.x instance settings to new values. + */ +function _phone_update_cck_phone_instance_settings(&$data) { + $data['module'] = 'phone'; + $data['widget']['module'] = 'phone'; + $data['widget']['type'] = 'phone_combo'; + $data['widget']['settings']['country_code_position'] = $data['settings']['country_code_position']; + unset($data['settings']['country_code_position']); + + // Convert all country codes to uppercase + $data['settings']['default_country'] = _phone_update_cck_phone_country($data['settings']['default_country']); + $orig_list = $data['settings']['country_codes']['country_selection']; + $data['settings']['country_codes']['country_selection'] = array(); + foreach ($orig_list as $orig_country => $value) { + $country = _phone_update_cck_phone_country($orig_country); + if (!empty($country)) { + $data['settings']['country_codes']['country_selection'][$country] = $value; + } + } + + // Update all formatters. + foreach ($data['display'] as $context => $ddata) { + if (is_array($ddata)) { + switch ($ddata['type']) { + case 'local': + $data['display'][$context]['type'] = 'phone_local'; + break; + case 'default': + default: + $data['display'][$context]['type'] = 'phone_global'; + break; + } + $data['display'][$context]['settings'] = $format_info[$data[$context]['type']]['settings']; + $data['display'][$context]['settings']['country_name_position'] = 'none'; + $data['display'][$context]['module'] = 'phone'; + } + } + + if (isset($data['settings']['enable_custom_country'])) { + $data['settings']['enable_country_level_validation'] = $data['settings']['enable_custom_country']; + unset($data['settings']['enable_custom_country']); + } + // @todo: what about "enable_mobile" option in D6? + + // Fill in any missing settings with default instance settings. + $info = phone_field_info(); + foreach ($info['phone']['settings'] as $setting => $value) { + if (!isset($data['settings'][$setting])) { + $data['settings'][$setting] = $value; + } + } +} + +/** + * Helper function for migrate/update functions: convert cck_phone-6.x and + * cck_phone-7.x-1.x country codes to new valus. + */ +function _phone_update_cck_phone_country($orig_country) { + $country = drupal_strtoupper($orig_country); + if ($country=='TP') { + // Replace TP with TL (Timor-Leste) + $country = 'TL'; + } + elseif ($country=='SS') { + // South Sudan has been removed + $country = NULL; + } + return $country; +} + +