diff --git a/commerce_addressbook.module b/commerce_addressbook.module
index 5b55699..22a0b61 100644
--- a/commerce_addressbook.module
+++ b/commerce_addressbook.module
@@ -294,7 +294,7 @@ function commerce_addressbook_commerce_checkout_pane_info_alter(&$panes) {
// which have the addressbook enabled.
if (!empty($pane['callbacks']['checkout_form'])) {
// We're free to insert our own info.
- $pane['commerce_addressbook']['overridden_form'] = $panes[$pane_id]['callbacks']['checkout_form'];
+ $pane['commerce_addressbook']['overridden_form'] = $pane['callbacks']['checkout_form'];
}
$pane['callbacks']['checkout_form'] = 'commerce_addressbook_profile_checkout_form';
}
@@ -312,9 +312,9 @@ function commerce_addressbook_profile_checkout_form($form, &$form_state, $checko
$profile_id = NULL;
if ($field_name = variable_get('commerce_' . $checkout_pane['pane_id'] . '_field', '')) {
// Try the related customer profile reference field...
- $wrapper = entity_metadata_wrapper('commerce_order', $order);
- if (isset($wrapper->{$field_name})) {
- $profile_id = $wrapper->{$field_name}->raw();
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $order);
+ if (isset($order_wrapper->{$field_name})) {
+ $profile_id = $order_wrapper->{$field_name}->raw();
}
}
else {
@@ -324,16 +324,21 @@ function commerce_addressbook_profile_checkout_form($form, &$form_state, $checko
}
}
- if (empty($profile_id)) {
- // Set default profile into the order, before the commerce_customer module
- // constructs its pane form. (The profile type is always part of the
- // pane_id with 'customer_profile_' removed; see commerce_customer.module.)
+ // If not filled, set default profile into the order, before the
+ // commerce_customer module constructs its pane form and automatically
+ // populates the profile fields. (The profile type is always part of the
+ // pane_id with 'customer_profile_' removed; see commerce_customer.module.)
+ // An 'addressbook' value of 0 is our internal sign to skip doing this:
+ if (empty($profile_id)
+ && (!isset($form_state['values'][$checkout_pane['pane_id']]['addressbook'])
+ || $form_state['values'][$checkout_pane['pane_id']]['addressbook'] !== 0)
+ ) {
$profile_id = commerce_addressbook_get_default_profile_id($user->uid, substr($checkout_pane['pane_id'], 17));
if ($profile_id) {
// Set in the profile reference field or the order's data array, as above.
if ($field_name) {
- $wrapper->{$field_name} = $profile_id;
+ $order_wrapper->{$field_name} = $profile_id;
}
else {
$order->data['profiles'][$checkout_pane['pane_id']] = $profile_id;
@@ -375,36 +380,18 @@ function commerce_addressbook_form_alter(&$form, &$form_state, $form_id) {
$checkout_page_id = substr($form_id, 23);
// Find all panes for our current checkout page.
foreach (commerce_checkout_panes(array('enabled' => TRUE, 'page' => $checkout_page_id)) as $pane_id => $checkout_pane) {
- // If this pane is a customer profile based pane build a list of previous
- // profiles from which to pick that are of the same bundle.
+ // If this pane is a customer profile based pane:,
+ // - build a select list of previous profiles (of the same bundle);
+ // - if 'copy profile' is enabled, check if the checkbox state is ok.
if (substr($pane_id, 0, 17) == 'customer_profile_' && isset($form[$pane_id]) && variable_get('commerce_' . $pane_id . '_addressbook', FALSE)) {
- $field = field_info_field(variable_get('commerce_' . $pane_id . '_field', ''));
- $profiles = commerce_customer_profile_load_multiple(array(), array('type' => $field['settings']['profile_type'], 'uid' => $user->uid, 'status' => TRUE));
+ $profiles = commerce_customer_profile_load_multiple(array(), array('type' => substr($pane_id, 17), 'uid' => $user->uid, 'status' => TRUE));
if ($profiles) {
- // Get the profile ID which will be selected by default:
- // - the ID submitted by a form;
- // - or the one present in the order. (If the order had no saved
- // profile, the default profile is set in the order already.)
- if (!empty($form_state['values'][$pane_id]['addressbook'])) {
- $default_value = $form_state['values'][$pane_id]['addressbook'];
- }
- else {
- $order_wrapper = entity_metadata_wrapper('commerce_order', $form_state['order']);
- if ($field_name = variable_get('commerce_' . $pane_id . '_field', '')) {
- // Try the related customer profile reference field...
- if (isset($order_wrapper->{$field_name})) {
- $default_value = $order_wrapper->{$field_name}->raw();
- }
- }
- else {
- // Or try the association stored in the order's data array.
- if (!empty($form_state['order']->data['profiles'][$pane_id])) {
- $default_value = $form_state['order']->data['profiles'][$pane_id];
- }
- }
- }
-
+ // Get the profile ID which will be selected by default. The profile
+ // is already stored in the form, and up to date. (After an AJAX
+ // request from the addressbook/copy element, it's been updated.)
+ $profile = $form[$pane_id]['customer_profile']['#value'];
+ $default_value = $profile->profile_id;
// Check if the default profile is in the enabled profiles array.
if (isset($default_value)) {
if (!isset($profiles[$default_value])) {
@@ -418,37 +405,71 @@ function commerce_addressbook_form_alter(&$form, &$form_state, $form_id) {
$default_value = 'none';
}
}
-
- // Make sure our profile type still exists..
- if (!empty($form[$pane_id]['commerce_customer_profile_copy'])) {
- if (($source_profile_type_name = variable_get('commerce_' . $pane_id . '_profile_copy_source', NULL))
- && $source_profile_type = commerce_customer_profile_type_load($source_profile_type_name)) {
- // Disable the profile copy checkbox.
- $form[$pane_id]['commerce_customer_profile_copy']['#default_value'] = FALSE;
- $form[$pane_id]['commerce_customer_profile_copy']['#access'] = FALSE;
- // Loop over source profile fields and enable previously disabled
- // fields.
- foreach (field_info_instances('commerce_customer_profile', $source_profile_type['type']) as $field_name => $field) {
- if (!empty($form[$pane_id][$field_name])){
- $langcode = $form[$pane_id][$field_name]['#language'];
- $form[$pane_id][$field_name][$langcode]['#access'] = TRUE;
- foreach (element_children($form[$pane_id][$field_name][$langcode]) as $key) {
- $form[$pane_id][$field_name][$langcode][$key]['#access'] = TRUE;
- // Disable the editing of the profile if the user doesn't
- // have the right to edit customer profiles.
- if (!commerce_customer_profile_access('update', $profiles[$default_value])) {
- $form[$pane_id][$field_name][$langcode][$key]['#disabled'] = TRUE;
- }
- }
- }
- }
- }
- }
}
else {
$default_value = 'none';
}
+ // Check if customer profile copying is enabled (and from where).
+ $extra_description = '';
+ if (!empty($form[$pane_id]['commerce_customer_profile_copy'])
+ && variable_get('commerce_' . $checkout_pane['pane_id'] . '_profile_copy', FALSE)
+ && ($source_type = variable_get('commerce_' . $pane_id . '_profile_copy_source', ''))) {
+
+ $source_pane_id = 'customer_profile_' . $source_type;
+
+ // Set extra validation callback for the 'profile copy' element that
+ // will update the target profile. Prepend it, so it runs before the
+ // regular callback saves the order.
+ array_unshift($form[$pane_id]['commerce_customer_profile_copy']['#element_validate'], 'commerce_addressbook_profile_copy_validate');
+
+ // Set extra after_build to catch $form_state['values'].
+ $form += array('#after_build' => array());
+ $form['#after_build'][] = 'commerce_addressbook_copy_after_build';
+
+ // One case where form input/value can differ from #default_value:
+ // form reload after a validation error.
+ $copy = isset($form_state['values'][$pane_id]['commerce_customer_profile_copy'])
+ ? $form_state['values'][$pane_id]['commerce_customer_profile_copy']
+ : $form[$pane_id]['commerce_customer_profile_copy']['#default_value'];
+
+ // The checkbox' original #default_value is set according to
+ // a property in the order data, or the configured default for this
+ // pane. In some cases, we want to compare the two profiles' data
+ // and derive the checkbox state from those.
+ // (This could be done in commerce_customer, but there an order's
+ // profile data is only set after explicit user action, so having
+ // the contents of profiles influence the checkbox value is not a
+ // real UX improvement for Commerce core. It is for us, because we
+ // set default values for profiles.)
+ // We check profiles:
+ // - on 'non-submit' requests (we compare the addressbook defaults);
+ // - in AJAX requests from an element on the source pane, IF
+ // the 'copy profile' checkbox is FALSE. (If TRUE, we want the
+ // changes to be copied to the target profile. If the address
+ // select list is the triggering element, we just changed the
+ // target profile + element in the validate callback.)
+ // - in AJAX requests from anywhere else but the source pane (other
+ // modules may have influenced profiles too), EXCEPT for a
+ // 'copy profile' checkbox.
+ $triggering_element_path = array('');
+ if (isset($form_state['triggering_element']['#parents'])) {
+ $triggering_element_path = $form_state['triggering_element']['#parents'];
+ }
+
+ if (end($triggering_element_path) !== 'commerce_customer_profile_copy'
+ && (!$copy || $triggering_element_path[0] !== $source_pane_id)) {
+ module_load_include('inc', 'commerce_addressbook', 'includes/commerce_addressbook.form');
+ $copy = _commerce_addressbook_check_copy_profile($form, $form_state, $pane_id, $source_type);
+ }
+
+ if ($copy) {
+ // Insert a little extra help text about conflicts between
+ // the 'addressbook' and 'copy' elements.
+ $extra_description = ' ' . t('This will negate the above checkbox value.');
+ }
+ }
+
// Prepare the options.
$options = array();
foreach ($profiles as $id => $profile) {
@@ -459,11 +480,12 @@ function commerce_addressbook_form_alter(&$form, &$form_state, $form_id) {
$form[$pane_id]['#prefix'] = '
';
$form[$pane_id]['#suffix'] = '
';
+ // Position the select list below the profile-copy checkbox.
$form[$pane_id]['addressbook'] = array(
'#addressbook' => TRUE,
'#type' => 'select',
'#title' => t('Addresses on File'),
- '#description' => t('You may select a pre-existing address on file.'),
+ '#description' => t('You may select a pre-existing address on file.') . $extra_description,
'#options' => $options,
'#empty_option' => t('-- Choose --'),
'#empty_value' => 'none',
@@ -472,7 +494,7 @@ function commerce_addressbook_form_alter(&$form, &$form_state, $form_id) {
'wrapper' => strtr($pane_id, '_', '-') . '-ajax-wrapper',
),
'#element_validate' => array('commerce_addressbook_saved_addresses_validate'),
- '#weight' => -100,
+ '#weight' => -29,
'#default_value' => $default_value,
);
}
@@ -480,7 +502,7 @@ function commerce_addressbook_form_alter(&$form, &$form_state, $form_id) {
}
}
- if ($form_id == 'commerce_checkout_pane_settings_form' && substr($form['checkout_pane']['#value']['pane_id'], 0, 16) == 'customer_profile') {
+ if ($form_id == 'commerce_checkout_pane_settings_form' && substr($form['checkout_pane']['#value']['pane_id'], 0, 17) == 'customer_profile_') {
$form['settings']['commerce_' . $form['checkout_pane']['#value']['pane_id'] . '_addressbook'] = array(
'#type' => 'checkbox',
'#title' => t('Enable the Address Book'),
@@ -503,6 +525,22 @@ function commerce_addressbook_form_alter(&$form, &$form_state, $form_id) {
}
/**
+ * after_build callback for a checkout form with addressbook elements and a
+ * 'copy profile' checkbox on it.
+ */
+function commerce_addressbook_copy_after_build($form, &$form_state) {
+ // Preserve $form_state['values'] built from input values, so
+ // _commerce_addressbook_merge_form_input() can read them when the form gets
+ // rebuilt, even if the values get cleared out upon form validation.
+ // (That's what e.g. an AJAX callback from an addressfield country list does.)
+ if (!empty($form_state['input'])) {
+ $form_state['rebuild_info']['addressbook_values'] = $form_state['values'];
+ }
+
+ return $form;
+}
+
+/**
* Ajax callback for replacing the appropriate commerce customer checkout pane.
*/
function commerce_addressbook_checkout_form_callback($form, $form_state) {
@@ -511,6 +549,14 @@ function commerce_addressbook_checkout_form_callback($form, $form_state) {
// Add replace element as default AJAX command.
$commands = array();
$commands[] = ajax_command_replace(NULL, drupal_render($form[$pane_id]));
+ // Add extra panes where the addressbook element was updated too; check that
+ // we don't re-render the same pane. (At the moment only when JS/AJAX callback
+ // is disabled, duplicate $pane_ids can be set.)
+ if (!empty($form_state['rebuild_info']['addressbook'])) {
+ foreach (array_unique($form_state['rebuild_info']['addressbook']) as $pane_id) {
+ $commands[] = ajax_command_replace('#' . strtr($pane_id, '_', '-') . '-ajax-wrapper', drupal_render($form[$pane_id]));
+ }
+ }
// Allow other modules to add arbitrary AJAX commands on the refresh.
drupal_alter('commerce_addressbook_callback', $commands, $form, $form_state);
@@ -522,10 +568,8 @@ function commerce_addressbook_checkout_form_callback($form, $form_state) {
* Element validate callback: processes input of the address select list.
*/
function commerce_addressbook_saved_addresses_validate($element, &$form_state, $form) {
- if (in_array('addressbook', $form_state['triggering_element']['#parents']) && $form_state['triggering_element']['#id'] == $element['#id']) {
+ if (in_array('addressbook', $form_state['triggering_element']['#parents'], TRUE) && $form_state['triggering_element']['#id'] == $element['#id']) {
$pane_id = $element['#parents'][0];
- $field_name = variable_get('commerce_' . $pane_id . '_field', '');
- $order_wrapper = entity_metadata_wrapper('commerce_order', $form_state['order']);
if (is_numeric($element['#value'])) {
global $user;
$profile = commerce_customer_profile_load($element['#value']);
@@ -534,24 +578,46 @@ function commerce_addressbook_saved_addresses_validate($element, &$form_state, $
drupal_set_message(t('You must own the profile you are choosing.'), 'error');
return;
}
- // If we detect a change in the element's value, and the customer profile
- // reference isn't already set to the specified value...
- if ($order_wrapper->{$field_name}->raw() != $element['#value']) {
- // Update the order based on the value and rebuild the form.
- if ($element['#value'] == 0) {
- $order_wrapper->{$field_name} = NULL;
- }
- else {
- $order_wrapper->{$field_name} = $element['#value'];
- }
- }
}
- else {
- $order_wrapper->{$field_name} = NULL;
+ module_load_include('inc', 'commerce_addressbook', 'includes/commerce_addressbook.form');
+ _commerce_addressbook_validate_change_profile($element['#value'], $pane_id, $form_state, $form);
+ }
+}
+
+/**
+ * Element validate callback: processes input of the copy checkbox.
+ */
+function commerce_addressbook_profile_copy_validate($element, &$form_state, $form) {
+ $triggering_element = end($form_state['triggering_element']['#parents']);
+
+ // Checkbox: On - invoked for the corresponding trigger element, or the
+ // "continue" checkout form button.
+ if ((($triggering_element == 'commerce_customer_profile_copy' && $form_state['triggering_element']['#id'] == $element['#id']) || $triggering_element == 'continue') && !empty($element['#value'])) {
+ $pane_id = $element['#parents'][0];
+
+ // Get values for source pane.
+ if (variable_get('commerce_' . $pane_id . '_profile_copy', FALSE)
+ && ($source_type = variable_get('commerce_' . $pane_id . '_profile_copy_source', ''))) {
+
+ // Find the target profile ID matching our (source) profile, and change
+ // it in the target addressfield element.
+ module_load_include('inc', 'commerce_addressbook', 'includes/commerce_addressbook.form');
+ $profile_id = _commerce_addressbook_profile_find_target($form, $form_state, $pane_id, 'customer_profile_' . $source_type);
+ _commerce_addressbook_validate_change_profile($profile_id, $pane_id, $form_state, $form);
+
+ // If the profile changed, the outdated/unused pane values are now
+ // cleared. On an AJAX request invoked by the checkbox element, the form
+ // rebuild will re-fill the profile, but on regular submit we should
+ // do it ourselves so the regular pane-validate function doesn't choke.
+ // (Note this never happens - unless the checkbox' #ajax was removed.)
+ if (!isset($profile_id)) {
+ $form_state['values'][$pane_id]['customer_profile'] = commerce_customer_profile_new(substr($pane_id, 17), $form_state['order']->uid);
+ }
+ elseif (!isset($form_state['values'][$pane_id]['customer_profile']->profile_id)
+ || $form_state['values'][$pane_id]['customer_profile']->profile_id != $profile_id) {
+ $form_state['values'][$pane_id]['customer_profile'] = commerce_customer_profile_load($profile_id);
+ }
}
- unset($form_state['input'][$pane_id]);
- $element_key = $form[$pane_id]['commerce_customer_address'][$form[$pane_id]['commerce_customer_address']['#language']][0]['element_key']['#value'];
- unset($form_state['addressfield'][$element_key]);
}
}
diff --git a/includes/commerce_addressbook.form.inc b/includes/commerce_addressbook.form.inc
new file mode 100644
index 0000000..b22c026
--- /dev/null
+++ b/includes/commerce_addressbook.form.inc
@@ -0,0 +1,523 @@
+ 'ajax', '#commands' => array());
+ }
+ else {
+ // Get result from the original callback.
+ $callback = $form_state['rebuild_info']['addressbook_original_ajax'];
+ $ajax_structure = $callback($form, $form_state);
+ }
+
+ if (isset($ajax_structure['#commands'])) {
+ // Add extra panes where the 'copy' checkbox was updated too.
+ if (!empty($form_state['rebuild_info']['addressbook'])) {
+ foreach ($form_state['rebuild_info']['addressbook'] as $pane_id) {
+ $ajax_structure['#commands'][] = ajax_command_replace('#' . strtr($pane_id, '_', '-') . '-ajax-wrapper', drupal_render($form[$pane_id]));
+ }
+ }
+ }
+ return $ajax_structure;
+ }
+}
+
+/**
+ * Returns a profile object for a pane form. This could be a new unsaved profile
+ * or NULL if not found.
+ * @param array $form
+ * @param array $form_state
+ * @param string $profile_type
+ * Type of profile (like 'billing'); it is not assumed that a corresponding
+ * checkout pane must exist on $form.
+ * @param bool $form_value_ok
+ * Set to FALSE if the 'customer_profile' #value in $form could be outdated.
+ * (i.e. when in a validate callback from the address select list.)
+ *
+ * @return object|null
+ */
+function _commerce_addressbook_get_profile(&$form, &$form_state, $profile_type, $form_value_ok = TRUE) {
+ $pane_id = 'customer_profile_' . $profile_type;
+ $profile = NULL;
+
+ // First try to get the entity as set in its pane form definition.
+ // (This could be a new unsaved profile.)
+ if ($form_value_ok && isset($form[$pane_id]['customer_profile']['#value'])) {
+ $profile = $form[$pane_id]['customer_profile']['#value'];
+ }
+ // It's possible that there is no pane for the source profile on this checkout
+ // page, so try to load a profile if not found.
+ elseif ($source_ref_field_name = variable_get('commerce_' . $pane_id . '_field', '')) {
+ // Try the related customer profile reference field...
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $form_state['order']);
+ if (isset($order_wrapper->{$source_ref_field_name})) {
+ $profile = $order_wrapper->{$source_ref_field_name}->value();
+ }
+ }
+ else {
+ // Or try the association stored in the order's data array.
+ if (!empty($form_state['order']->data['profiles'][$profile_type])) {
+ $profile = commerce_customer_profile_load($form_state['order']->data['profiles'][$profile_type]);
+ }
+ }
+
+ return $profile;
+}
+
+/**
+ * Helper function for form_alter; checks what value we want the 'copy' checkbox
+ * to have. If that differs from the actual value: changes it, adjusts other
+ * properties and enables/disables profile fields if needed.
+ *
+ * Returns boolean for the (new) checkbox value; NULL if no check was done.
+ */
+function _commerce_addressbook_check_copy_profile(&$form, &$form_state, $pane_id, $source_type) {
+ $source_pane_id = 'customer_profile_' . $source_type;
+
+ // Get main (target) profile. (Just as in caller.)
+ $profile = $form[$pane_id]['customer_profile']['#value'];
+ // Get source profile. (We don't know if this #value is on the form.)
+ $source_profile = _commerce_addressbook_get_profile($form, $form_state, $source_type);
+
+ // Determine what the value of the 'copy' checkbox should be.
+
+ // Skip the comparison if both profiles are new. (Not if one of them is new;
+ // if you have two 'equal' addresses select from the addressbook and you
+ // select the "none" option, the copy checkbox should be cleared.)
+ if (isset($source_profile)
+ && (!empty($profile->profile_id) || !empty($source_profile->profile_id))) {
+
+ // If the 'copy' value we determined is different from the checkbox'
+ // default, take action.
+
+ $copy = _commerce_addressbook_profiles_equal($profile, $source_profile, $form, $form_state, $pane_id, 'customer_profile_' . $source_type);
+ $default = isset($form_state['values'][$pane_id]['commerce_customer_profile_copy'])
+ ? $form_state['values'][$pane_id]['commerce_customer_profile_copy']
+ : $form[$pane_id]['commerce_customer_profile_copy']['#default_value'];
+
+ if ($copy == empty($default)) {
+
+ $form[$pane_id]['commerce_customer_profile_copy']['#default_value'] = $copy;
+ // Unset the input value to
+ // - always make #default_value displayed, if the form gets rebuilt.
+ // on AJAX. ('input' was already emptied for a request triggered by
+ // the checkbox, but not for an address select list on the source pane.)
+ // - prevent a copying action, if it is set. Note:
+ // * We choose not to do the reverse; never set any 'copy' value if it's
+ // currently unset. We don't want to risk triggering a copying action
+ // without having shown this to the website user.
+ // * This is just a precaution. There is no regular situation where
+ // changing $form_state['input'] has any effect, because
+ // - regular form submits don't execute this code (form is cached);
+ // - AJAX requests will not process input after this; only rerender.
+ unset($form_state['input'][$pane_id]['commerce_customer_profile_copy']);
+
+ // Sometimes (specifically, on every change not caused by an address
+ // select list) we choose to leave the profile fields enabled, when
+ // turning on the checkbox, because hiding everything at the moment you do
+ // something unrelated seems bad UX.
+ // (So effectively, then the 'copy' checkbox becomes just an indicator
+ // whether a profile is equal.)
+ $skip_hide = FALSE;
+ if ($copy && !empty($form_state['input']) && !in_array('addressbook', $form_state['triggering_element']['#parents'], TRUE)) {
+ // Not hiding elements means there can be input conflicts: when 'copy'
+ // is checked, all submitted profile fields will be discarded.
+ // Account for this situation by warning in the checkbox description.
+ // (NOTE: we should account for this situation anyway, if we want to
+ // support this module in non-JS environments.
+ // Finishing that support is left as a TODO.)
+ $form[$pane_id]['addressbook_warning'] = array(
+ '#markup' => t("WARNING: any changes made to the fields below will be discarded, unless the 'is the same' checkbox is unchecked."),
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#weight' => -100,
+ );
+ $skip_hide = TRUE;
+ }
+ // Loop over source profile fields and enable/disable previously
+ // disabled/enabled fields, mimicking commerce_customer module behavior:
+ // - we take the source instances (not target) because that's what
+ // commerce_customer_profile_pane_checkout_form() does too.
+ // - we prefer taking $form_state['values'] over $source_profile for
+ // setting 'elements' because commerce_customer_profile_copy_fields()
+ // does that too - and there's a difference: if a field is empty,
+ // the submitted form value is (LANGUAGE => (0 => "")) while
+ // $source_profile->{$field_name} is an empty array. The first
+ // disables the destination field; the second won't.
+ $source = isset($form_state['values'][$source_pane_id]['customer_profile']) ? (object) $form_state['values'][$source_pane_id] : $source_profile;
+ foreach (field_info_instances('commerce_customer_profile', $source_type) as $field_name => $field_def) {
+ if (!empty($form[$pane_id][$field_name])){
+
+ // - Disable / enable fields, and
+ // - Note in the order that the element value is copied from the
+ // source; this makes commerce_customer module keep the element
+ // disabled on form rebuild.
+ // Code copied from commerce_customer_profile_copy_fields() & changed.
+ $field = array();
+ if (isset($source->$field_name) && is_array($source->$field_name)) {
+ $field = $source->$field_name;
+ foreach ($field as $langcode => $items) {
+
+ if (is_array($items)) {
+ foreach ($items as $delta => $item) {
+ // from commerce_customer_profile_pane_checkout_form():
+ if (!$skip_hide && isset($form[$pane_id][$field_name][$langcode][$delta])) {
+ $form[$pane_id][$field_name][$langcode][$delta]['#access'] = !$copy;
+ }
+
+ if ($copy) {
+ $form_state['order']->data['profile_copy'][$pane_id]['elements'][$field_name][$langcode][$delta] = TRUE;
+ }
+ }
+ }
+ elseif ($copy) {
+ $form_state['order']->data['profile_copy'][$pane_id]['elements'][$field_name][$langcode] = TRUE;
+ }
+ }
+ }
+ // commerce_customer_profile_pane_checkout_form() is inconsistent; if
+ // $copy == FALSE here, it has sometimes disabled field_name->und->0
+ // and sometimes field_name->und. (The latter was done when no order
+ // 'copy' properties are set yet. So check for #access also when
+ // $items is an array.
+ $langcode = !empty($form[$pane_id][$field_name]['#language']) ? $form[$pane_id][$field_name]['#language'] : '';
+ if ((!$field && !$skip_hide && isset($form[$pane_id][$field_name][$langcode]))
+ || isset($form[$pane_id][$field_name][$langcode]['#access'])) {
+ $form[$pane_id][$field_name][$langcode]['#access'] = !$copy;
+ }
+ }
+ }
+
+ if (!$copy) {
+ // Unset values in the order that say which elements are copied.
+ unset($form_state['order']->data['profile_copy'][$pane_id]['elements']);
+ }
+ // Change status. (We keep status = FALSE in the order even when unsetting
+ // 'elements', just like commerce_customer does.)
+ $form_state['order']->data['profile_copy'][$pane_id]['status'] = $copy;
+
+ // Save the order. (Since $form_state['order'] is not going to be used by
+ // the pane validation, we need to either make sure that the changed order
+ // properties are saved here, or re-apply the changes in the pane
+ // validate / submit callback. The latter is too much work.)
+ commerce_order_save($form_state['order']);
+
+ if (isset($form_state['triggering_element']['#ajax'])) {
+ if (in_array('addressbook', $form_state['triggering_element']['#parents'], TRUE)) {
+ // The triggering element is the address select list. Only if it is
+ // on another pane, we need to instruct the render callback to also
+ // render this pane with the checkbox.
+ if ($form_state['triggering_element']['#parents'][0] !== $pane_id) {
+ $form_state += array('addressbook' => array());
+ $form_state['rebuild_info']['addressbook'][] = $pane_id;
+ }
+ }
+ else {
+ // The triggering element is unknown. (Likely candidate: country.)
+ // We can't influence the AJAX wrapper div anymore, only the callback.
+ // Put a 'wrapper' around the current callback to also redraw our pane.
+ $path = array_merge($form_state['triggering_element']['#parents'], array('#ajax', 'callback'));
+ if (!isset($form_state['rebuild_info']['addressbook_original_callback'])) {
+ // Store original callback; change it to our wrapper.
+ $form_state['rebuild_info']['addressbook_original_ajax'] =
+ drupal_array_get_nested_value($form, $path);
+ drupal_array_set_nested_value($form, $path, 'commerce_addressbook_ajax_callback_wrapper');
+ }
+ // If the triggering element is on this pane, then don't redraw it
+ // twice; 'cancel' the current one.
+ if ($form_state['triggering_element']['#parents'][0] == $pane_id) {
+ $form_state['rebuild_info']['addressbook_original_ajax_cancel'] = TRUE;
+ }
+
+ // In addition, mark our pane for being redrawn.
+ $form_state['rebuild_info'] += array('addressbook' => array());
+ $form_state['rebuild_info']['addressbook'][] = $pane_id;
+ }
+ }
+ }
+ return $copy;
+ }
+}
+
+/**
+ * Returns boolean indicating whether a target/source profile have equal fields.
+ * (Two new profiles do not compare as 'equal'.)
+ */
+function _commerce_addressbook_profiles_equal($profile, $source_profile, $form, $form_state, $pane_id, $source_pane_id, $skip_dest_input = FALSE) {
+ // This function is split out from the form_alter only to make it shorter
+ // (and because the details of field extraction / comparison have nothing to
+ // do with surrounding functionality); it's not meant to be general.
+
+ // If there's input, we are in an AJAX-triggered rebuild (or the form has
+ // errors): Form values for the profile fields are not validated or updated
+ // into the entity, but they are available. We want to compare the profiles
+ // with up to date field values.
+ // (The primary purpose of this function is just to check whether two selected
+ // / default profiles are equal. But in edge cases, it may be less confusing
+ // if the value of the checkbox / target profile -which will be set based
+ // on this- follows the current values on the form.)
+ // So, get those form values into the entity, unless earlier processing
+ // (from address select list?) has unset input for this pane.
+ if (!$skip_dest_input && isset($form_state['input'][$pane_id])) {
+ $profile = _commerce_addressbook_merge_form_input($profile, $form, $form_state, $pane_id);
+ }
+ if (isset($form_state['input'][$source_pane_id])) {
+ $source_profile = _commerce_addressbook_merge_form_input($source_profile, $form, $form_state, $source_pane_id);
+ }
+
+ if (empty($profile->profile_id) || empty($source_profile->profile_id)) {
+ return FALSE;
+ }
+
+ // Check if all fields in the 'target' profile are present and equal in the
+ // 'source' profile. (This code is built to match how
+ // commerce_customer_profile_copy_fields() works, but needed to be extended
+ // to accommodate submitted form values vs. profile fields.)
+
+ // Loop over all the field instances that could be attached to this target.
+ foreach (field_info_instances('commerce_customer_profile', substr($pane_id, 17))
+ as $field_name => $instance) {
+ $field = $source_profile->{$field_name};
+
+ // Loop over the source field value and see if its items are equal.
+ // Take into account that array( LANG => array() ) should count as empty.
+ if (empty($field) || (is_array($field) && !array_filter($field))) {
+ if (!empty($profile->{$field_name})
+ && (!is_array($profile->{$field_name}) || array_filter($profile->{$field_name}))) {
+ return FALSE;
+ }
+ }
+ elseif (!is_array($field)) {
+ if (empty($profile->{$field_name}) || $profile->{$field_name} !== $field) {
+ return FALSE;
+ }
+ }
+ else {
+ foreach ($field as $langcode => $items) {
+ if (empty($items)) {
+ if (!empty($profile->{$field_name}[$langcode])) {
+ return FALSE;
+ }
+ }
+ else {
+ if (empty($profile->{$field_name}[$langcode])) {
+ return FALSE;
+ }
+ if (is_array($items) && is_array($profile->{$field_name}[$langcode])) {
+ if (array_map('_commerce_addressbook_filter_field_properties', $profile->{$field_name}[$langcode])
+ !== array_map('_commerce_addressbook_filter_field_properties', $items)) {
+ return FALSE;
+ }
+ }
+ elseif ($profile->{$field_name}[$langcode] !== $items) {
+ return FALSE;
+ }
+ }
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Update a profile with $form_state values; only for comparison purposes,
+ * the profile is not meant to be used/stored after this.
+ */
+function _commerce_addressbook_merge_form_input($profile, $form, $form_state, $pane_id) {
+ global $user;
+
+ $entity = isset($profile) ? clone $profile :
+ commerce_customer_profile_new(substr($pane_id, 17), $user->uid);
+
+ // We saved the full input values (regardless whether they're correctly
+ // validated); if $form_state['values'] is empty, use these for comparison.
+ // Notes:
+ // - in an AJAX request from an address select list, these input values are
+ // outdated for the corresponding pane - but this function doesn't
+ // get called for that pane;
+ // - if $form_state['values'] is still filled, prefer these; validation may
+ // have changed them to be more accurate;
+ // - $form_state is not passed by reference.
+ if (!empty($form_state['rebuild_info']['addressbook_values'][$pane_id]) && empty($form_state['values'][$pane_id])) {
+ $form_state['values'][$pane_id] = $form_state['rebuild_info']['addressbook_values'][$pane_id];
+ }
+
+ // Now add $form_state['values'] to the entity. (Is not set in edge cases
+ // where form data is posted but the $form_state cache was emptied.)
+ if (isset($form_state['values'])) {
+ // Part of field_attach_form_validate():
+ _field_invoke_default('extract_form_values', 'commerce_customer_profile', $entity, $form, $form_state);
+ try {
+ field_attach_validate('commerce_customer_profile', $entity);
+
+ entity_form_submit_build_entity('commerce_customer_profile', $entity, $form[$pane_id], $form_state);
+ // This is necessary to populate field properties that may exist
+ // already in the other profile's database-loaded fields:
+ _field_invoke_multiple('load', 'commerce_customer_profile', array($entity->profile_id => $entity));
+ $profile = $entity;
+ }
+ catch (Exception $e) {
+ // Just return $profile with no values updated.
+ }
+ }
+
+ return $profile;
+}
+
+
+/**
+ * Callback for array_map(); removes unwanted properties from field items,
+ * so fields from 'submitted values' can be compared with fields loaded from
+ * a database.
+ */
+function _commerce_addressbook_filter_field_properties($item) {
+ if (is_array($item)) {
+ // Even after invoking hook_field_load (which populates extra properties in
+ // the 'submitted' values), there are still differences:
+ foreach ($item as $key => $value) {
+ // - db-loaded values can have properties set from default database fields
+ // in the SQL table which are not present in submitted values.
+ // (Example: ('format' => NULL) for almost all text fields.)
+ // Filter out NULLs. (Note this doesn't account for database fields with
+ // non-null default values, but we don't know of any.)
+ // - that means we should remove properties with value '' (not 0) too.
+ // db-loaded values of NULL often correspond to submitted values of ''.
+ if (!isset($value) || $value === '') {
+ unset($item[$key]);
+ }
+ }
+ // - submitted addressfield values have an "element_key" property which is
+ // used only for form element handling; not present in the db-loaded value
+ unset($item['element_key']);
+ ksort($item);
+ }
+ return $item;
+}
+
+/**
+ * Helper function for element validate callbacks; if a profile ID has changed
+ * for a certain pane, change the appropriate values in $form_state to be
+ * consistent with the new ID & ready for a form rebuild.
+ */
+function _commerce_addressbook_validate_change_profile($new_profile_id, $pane_id, &$form_state, $form) {
+
+ // Get existing field
+ if ($field_name = variable_get('commerce_' . $pane_id . '_field', '')) {
+ // Try the related customer profile reference field...
+ $order_wrapper = entity_metadata_wrapper('commerce_order', $form_state['order']);
+ if (isset($order_wrapper->{$field_name})) {
+ $profile_id = $order_wrapper->{$field_name}->raw();
+ }
+ }
+ else {
+ // Or try the association stored in the order's data array.
+ if (!empty($form_state['order']->data['profiles'][$pane_id])) {
+ $profile_id = $form_state['order']->data['profiles'][$pane_id];
+ }
+ }
+
+ if ($new_profile_id === 'none') {
+ $new_profile_id = NULL;
+ }
+ if ($new_profile_id != $profile_id) {
+
+ // Set changed value in the profile reference field or the order's data
+ // array, as above. This will take care of setting the right value in
+ // the form rebuild, if we're in an AJAX request.
+ if ($field_name) {
+ $order_wrapper->{$field_name} = $new_profile_id;
+ }
+ else {
+ $form_state['order']->data['profiles'][$pane_id] = $new_profile_id;
+ }
+ if (empty($new_profile_id)) {
+ // Signify to commerce_addressbook_profile_checkout_form() that it should
+ // leave the profile empty instead of filling the default profile.
+ // ($form_state['values'] is still present during the form build callback
+ // but will be lost and rebuilt shortly afterwards, so this is an OK way.)
+ $form_state['values'][$pane_id]['addressbook'] = 0;
+ }
+
+ // Remove input fields for this customer profile, which is outdated now the
+ // selected profile itself has changed. (An AJAX request from the
+ // address select list overrides any input in other fields; the form rebuild
+ // will populate the fields from the new profile, set above.)
+ unset($form_state['input'][$pane_id]);
+ $element_key = $form[$pane_id]['commerce_customer_address'][$form[$pane_id]['commerce_customer_address']['#language']][0]['element_key']['#value'];
+ unset($form_state['addressfield'][$element_key]);
+ // If we're resetting an invisible 'target' addressfield, then probably
+ // addressfield_standard_country_validate() will still run after us and
+ // REpopulate $form_state['addressfield'] from the outdated 'values' (which
+ // causes the outdated address to reappear, now or when you untick the
+ // 'copy' checkbox). Work around this; just unset all outdated 'values'.
+ $form_state['values'][$pane_id] = array_intersect_key($form_state['values'][$pane_id], array('addressbook' => TRUE));
+
+ // Loop through all checkout panes on this form, and see if any panes
+ // copy their information from this one & have an address select list.
+ // If so, we also need to change that pane's profile value.
+ // Find all panes for our checkout page.
+ $checkout_page_id = substr($form['#form_id'], 23);
+ foreach (commerce_checkout_panes(array('enabled' => TRUE, 'page' => $checkout_page_id)) as $target_pane_id => $checkout_pane) {
+ // Is a (different) pane form?
+ if (isset($form[$target_pane_id]) && $target_pane_id != $pane_id
+ // ...which has addressbook functionality enabled?
+ && substr($target_pane_id, 0, 17) == 'customer_profile_' && variable_get('commerce_' . $target_pane_id . '_addressbook', FALSE)
+ // ...and has the current pane as a source?
+ && variable_get('commerce_' . $target_pane_id . '_profile_copy', FALSE)
+ && variable_get('commerce_' . $target_pane_id . '_profile_copy_source', '') == substr($pane_id, 17)
+ // ...and the 'copy' checkbox is ON? (form values are populated.)
+ && !empty($form_state['values'][$target_pane_id]['commerce_customer_profile_copy'])
+ ) {
+ $profile_id = _commerce_addressbook_profile_find_target($form, $form_state, $target_pane_id, $pane_id);
+ _commerce_addressbook_validate_change_profile($profile_id, $target_pane_id, $form_state, $form);
+ // Note that this pane should also be rerendered by the AJAX callback.
+ // We should use a form_state property that isn't cached; rebuild_info
+ // seems a good fit.
+ $form_state['rebuild_info'] += array('addressbook' => array());
+ $form_state['rebuild_info']['addressbook'][] = $target_pane_id;
+ }
+ }
+ }
+}
+
+/**
+ * Finds target profile matching our source input/profile, returns ID or NULL.
+ */
+function _commerce_addressbook_profile_find_target(&$form, &$form_state, $pane_id, $source_pane_id) {
+ global $user;
+
+ $source_profile = _commerce_addressbook_get_profile($form, $form_state, substr($source_pane_id, 17), FALSE);
+
+ $profiles = commerce_customer_profile_load_multiple(array(), array('type' => substr($pane_id, 17), 'uid' => $user->uid, 'status' => TRUE));
+ foreach ($profiles as $id => $profile) {
+ // Compare $profile with our source profile; do not incorporate form input
+ // for the _target_ profile.
+ if (_commerce_addressbook_profiles_equal($profile, $source_profile, $form, $form_state, $pane_id, $source_pane_id, TRUE)) {
+ return $id;
+ }
+ }
+}