conflict.module in Conflict 7
Same filename and directory in other branches
Fieldwise conflict prevention and resolution. @author Brandon Bergren
File
conflict.moduleView source
<?php
/**
* @file
* Fieldwise conflict prevention and resolution.
* @author Brandon Bergren
*/
/**
* @todo
* - Figure out what to do about dependencies between fields?
* - Come up with a cleaner way to compare fields than the current hashing.
* - Figure out a way to inform the user as to what the other side of the
* conflict is?
* - Handle things that aren't fields?
* - Handle non-nodes?
*/
/**
* Implements hook_form_FORM_ID_alter().
*/
function conflict_form_node_form_alter(&$form, &$form_state) {
// Force caching enabled so the original $form['node'] survives multiple page
// requests.
if (variable_get('conflict_enable_' . $form['#node']->type, FALSE)) {
$form_state['cache'] = TRUE;
}
// Highlight conflict or expired fields.
if (!empty($form_state['temporary']['conflict_fields'])) {
foreach ($form_state['temporary']['conflict_fields'] as $field_name) {
_conflict_apply_error($form[$field_name]);
}
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function conflict_form_node_type_form_alter(&$form, &$form_state) {
$form['submission']['conflict_enable'] = array(
'#type' => 'checkbox',
'#title' => t('Conflict resolution'),
'#description' => t('Enable field conflict resolution for this content type'),
'#default_value' => variable_get('conflict_enable_' . $form['#node_type']->type, FALSE),
);
}
/**
* Implements hook_node_validate().
*/
function conflict_node_validate($node, &$form, &$form_state) {
if (isset($node->nid) && node_last_changed($node->nid) > $node->changed && variable_get('conflict_enable_' . $node->type, FALSE)) {
// We only support nodes for now.
$entity_type = 'node';
// Bypass the core conflict detector.
$errors =& drupal_static('form_set_error', array());
if (!empty($errors['changed'])) {
unset($errors['changed']);
// Remove the message as well.
foreach ($_SESSION['messages']['error'] as $k => $v) {
if ($v == t('The content on this page has either been modified by another user, or you have already submitted modifications using this form. As a result, your changes cannot be saved.')) {
unset($_SESSION['messages']['error'][$k]);
$_SESSION['messages']['error'] = array_values($_SESSION['messages']['error']);
}
if (empty($_SESSION['messages']['error'])) {
unset($_SESSION['messages']['error']);
}
}
}
$nodes = array();
// Base node: The common ancestor that was cached when beginning the node
// edit.
$nodes['base'] = clone $form_state['node'];
// Theirs: The current state of the node, with the changes that happened in
// parallel.
$nodes['theirs'] = clone node_load($node->nid, NULL, TRUE);
// This workaround is done because the $node object in this hook does not
// have processed fields and would be inconsistent during comparison.
_field_invoke_multiple('load', 'node', array(
$nodes['theirs']->nid => $nodes['theirs'],
));
// Mine: The node that was about to be saved.
$tmp_form_state = $form_state;
$nodes['mine'] = node_form_submit_build_node($form, $tmp_form_state);
_field_invoke_multiple('load', 'node', array(
$nodes['mine']->nid => $nodes['mine'],
));
// Store fields names to highlight them after form rebuild.
$error_fields = array();
// Dig through the fields looking for conflicts.
$updated = FALSE;
$remote_changes = _conflict_get_node_changes($nodes['base'], $nodes['theirs']);
if (!empty($remote_changes)) {
$local_changes = _conflict_get_node_changes($nodes['mine'], $nodes['base']);
$real_changes = _conflict_get_node_changes($nodes['mine'], $nodes['theirs']);
$conflicted_fields = array_intersect($local_changes, $remote_changes, $real_changes);
$changed_fields = array_diff($remote_changes, array_intersect($local_changes, $remote_changes));
foreach ($conflicted_fields as $field_name => $field_title) {
// If the field can have unlimited values, check if the added values
// from mine & theirs can be merged.
$field = field_info_field($field_name);
if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED && $field['type'] != 'taxonomy_term_reference') {
// todo handling taxonomy_term_reference fields would be nice.
$field_language = field_language('node', $form_state['node'], $field_name);
$columns = array_keys($field['columns']);
$same = TRUE;
foreach ($nodes['base']->{$field_name}[$field_language] as $delta => $value) {
foreach ($columns as $column) {
if (!isset($nodes['mine']->{$field_name}[$field_language][$delta][$column]) || $value[$column] != $nodes['mine']->{$field_name}[$field_language][$delta][$column] || !isset($nodes['theirs']->{$field_name}[$field_language][$delta][$column]) || $value[$column] != $nodes['theirs']->{$field_name}[$field_language][$delta][$column]) {
$same = FALSE;
break;
}
}
}
if ($same) {
// The values in base match both mine & theirs. Merge added values.
unset($form_state['input'][$field_name]);
$nodes['theirs']->{$field_name}[$field_language] = array_merge($nodes['theirs']->{$field_name}[$field_language], array_slice($nodes['mine']->{$field_name}[$field_language], $delta + 1));
drupal_set_message(t('The @label field was also changed by another user. Your additions have been merged together. Please verify the updated values and save.', [
'@label' => $field_title,
]), 'warning');
$updated = TRUE;
continue;
}
}
drupal_set_message(t('The @label field was changed by another user while you were editing it. Save again to overwrite it.', array(
'@label' => $field_title,
)), 'error');
$updated = TRUE;
$error_fields[] = $field_name;
}
foreach ($changed_fields as $field_name => $field_title) {
// Forget the user-submitted value.
unset($form_state['input'][$field_name]);
drupal_set_message(t('The @label field was changed by another user. Please verify the updated value.', array(
'@label' => $field_title,
)), 'warning');
$updated = TRUE;
$error_fields[] = $field_name;
}
}
if ($updated) {
// Reload the node to pick up the updated data.
$node = clone $nodes['theirs'];
node_object_prepare($node);
$form_state['node'] = $node;
$form_state['rebuild'] = TRUE;
$form_state['temporary']['conflict_fields'] = $error_fields;
}
}
}
/**
* Iterate all elements children recursively and set error class.
*
* @param array $elements
*/
function _conflict_apply_error(&$elements) {
// Iterate through all children.
foreach (element_children($elements) as $key) {
if (isset($elements[$key]) && $elements[$key]) {
_conflict_apply_error($elements[$key]);
}
}
// If there are properties, it's probably safe to set #attributes here.
// Ignore container elements, because 'error' class adds a weird style there.
$element_properties = element_properties($elements);
if (!empty($element_properties) && (isset($elements['#type']) && $elements['#type'] != 'container')) {
$elements['#attributes']['class'][] = 'error';
}
}
/**
* Helper function. Get differences between two nodes.
*/
function _conflict_get_node_changes($node, $unchanged) {
$items = array();
$bundle = field_extract_bundle('node', $node);
$instances = field_info_instances('node', $bundle);
$properties = array(
'title' => array(
'field_name' => 'title',
'label' => t('Title'),
),
'created' => array(
'field_name' => 'date',
'label' => t('Authored on date'),
),
'uid' => array(
'field_name' => 'name',
'label' => t('Authored by'),
),
);
foreach ($properties as $property => $property_value) {
$same = $node->{$property} == $unchanged->{$property};
if (!$same) {
$items[$property_value['field_name']] = $property_value['label'];
}
}
foreach (field_info_instances('node', $node->type) as $field_name => $instance) {
$field = field_info_field($field_name);
$field_language = field_language('node', $node, $field_name);
$old_value = empty($unchanged->{$field_name}[$field_language]) ? NULL : $unchanged->{$field_name}[$field_language];
if (!empty($old_value) && $field['type'] == 'taxonomy_term_reference') {
// Thanks to core being inconsistent, $node->original only contains
// taxonomy term IDs (tids) for term reference fields, whereas $node
// itself contains everything (tid, name, description, etc). We need to
// store the full data as part of the nodechanges field here, since when
// it comes time to display, if a term has since been removed from the
// system, we can no longer load it at that point. So, we load
// everything here to make sure we've got it all for now and forever.
$tids = array();
foreach ($old_value as $key => $value) {
if (empty($value['tid'])) {
unset($old_value[$key]);
}
else {
$tids[] = $value['tid'];
}
}
if (!empty($tids)) {
$terms = taxonomy_term_load_multiple($tids);
foreach ($old_value as $key => $value) {
$tid = $value['tid'];
if (!empty($terms[$tid])) {
$old_value[$key] = (array) $terms[$tid];
}
}
}
}
$new_value = empty($node->{$field_name}[$field_language]) ? NULL : $node->{$field_name}[$field_language];
if (!empty($new_value) && $field['type'] == 'image') {
// uri is required to show image, see theme_image_formatter(), but does
// not exist in new value of image field.
foreach ($new_value as $key => $value) {
$new_value[$key]['uri'] = file_load($value['fid'])->uri;
}
}
$same = empty($old_value) == empty($new_value);
if ($same && !empty($old_value)) {
$old_deltas = array_keys($old_value);
$new_deltas = array_keys($new_value);
$same = !array_diff($old_deltas, $new_deltas) && !array_diff($new_deltas, $old_deltas);
if ($same) {
$columns = array_keys($field['columns']);
foreach ($old_value as $delta => $old_items) {
foreach ($columns as $column) {
$set = isset($old_items[$column]);
if ($set != isset($new_value[$delta][$column]) || $set && $old_items[$column] != $new_value[$delta][$column]) {
$same = FALSE;
break;
}
}
}
}
}
if (!$same) {
$items[$field_name] = $instances[$field_name]['label'];
}
}
return $items;
}
Functions
Name | Description |
---|---|
conflict_form_node_form_alter | Implements hook_form_FORM_ID_alter(). |
conflict_form_node_type_form_alter | Implements hook_form_FORM_ID_alter(). |
conflict_node_validate | Implements hook_node_validate(). |
_conflict_apply_error | Iterate all elements children recursively and set error class. |
_conflict_get_node_changes | Helper function. Get differences between two nodes. |