comment_alter.module in Comment Alter 7
Same filename and directory in other branches
Provides UI to alter nodes' parameters from comment forms.
@author CSÉCSY László <boobaa@kybest.hu>
File
comment_alter.moduleView source
<?php
/**
* @file
* Provides UI to alter nodes' parameters from comment forms.
*
* @author
* CSÉCSY László <boobaa@kybest.hu>
*/
/**
* Implements hook_form_FORM_ID_alter().
*
* @see comment_alter_form_field_ui_field_edit_form_alter_submit()
*/
function comment_alter_form_field_ui_field_edit_form_alter(&$form, &$form_state, $form_id) {
// Image fields fields are not supported by now, peroid.
// Field collection not currently supported by Diff module.
// @see http://drupal.org/node/1897196
if ($form['#field']['type'] == 'image' || $form['#field']['type'] == 'field_collection') {
return;
}
if (isset($form['instance']) && $form['instance']['entity_type']['#value'] == 'node') {
$bundle_name = $form['instance']['bundle']['#value'];
$field_name = $form['instance']['field_name']['#value'];
$comment_fields = field_info_instances('comment', 'comment_node_' . $bundle_name);
// Additional custom field values will be automatically stored in the
// database, so custom save mechanism is not necessary.
$form['instance']['comment_alter'] = array(
'#type' => 'checkbox',
'#title' => t('Enable altering this field from comments.'),
'#weight' => $form['instance']['required']['#weight'] + 0.5,
'#default_value' => !empty($form['#instance']['comment_alter']),
);
$form['instance']['comment_alter_hide'] = array(
'#type' => 'checkbox',
'#title' => t('Hide alterations of this field from diffs.'),
'#weight' => $form['instance']['required']['#weight'] + 0.7,
'#default_value' => !empty($form['#instance']['comment_alter_hide']),
'#states' => array(
'invisible' => array(
':input[name="instance[comment_alter]"]' => array(
'checked' => FALSE,
),
),
),
);
// Make sure the field isn't already on the Comment entity.
if (!empty($comment_fields[$field_name])) {
$form['instance']['comment_alter']['#default_value'] = FALSE;
$form['instance']['comment_alter']['#disabled'] = TRUE;
$form['instance']['comment_alter']['#description'] = '<span class="error">' . t('This field also exists on the comment. You must remove it from the comment in order to enable altering this field from comments.') . '</span>';
}
}
}
/**
* Returns the comment-alterable fields for a content type.
*
* @param string $content_type
* Node object, at least with the type property.
* @return array
* Array of the comment-alterable fields for that content type.
*/
function comment_alter_get_alterable_fields($content_type) {
$comment_alter_fields =& drupal_static(__FUNCTION__);
if (!isset($comment_alter_fields[$content_type])) {
$field_infos = field_info_instances('node', $content_type);
$comment_fields = field_info_instances('comment', 'comment_node_' . $content_type);
$comment_alter_fields[$content_type] = array();
foreach ($field_infos as $field_name => $value) {
if (!empty($value['comment_alter']) && empty($comment_fields[$field_name])) {
$comment_alter_fields[$content_type][] = $field_name;
}
}
}
return $comment_alter_fields[$content_type];
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Don't allow users to add existing fields if they are alterable from comment
* on the parent node bundle.
*/
function comment_alter_form_field_ui_field_overview_form_alter(&$form, &$form_state) {
if ($form['#entity_type'] == 'comment') {
// Remove 'comment_node_' from the beginning of the bundle name to get the
// node bundle name.
$node_bundle_name = substr($form['#bundle'], 13);
$comment_alter_fields = comment_alter_get_alterable_fields($node_bundle_name);
if (!empty($comment_alter_fields)) {
$field_options =& $form['fields']['_add_existing_field']['field_name']['#options'];
foreach ($field_options as $field_name => $field_label) {
if (in_array($field_name, $comment_alter_fields)) {
// Remove fields that are alterable from comment on the parent node.
unset($field_options[$field_name]);
}
}
}
}
}
/**
* Implements hook_requirements().
*
* If somehow a field is added to a comment bundle which is also alterable from
* comment on the parent node bundle, put a message on the system status report.
*
* We've made it impossible to do this via the UI, but it's still possible via
* low-level trickery, so this is a fallback to alert site administrators of a
* problem.
*/
function comment_alter_requirements($phase) {
$requirements = array();
if ($phase == 'runtime') {
foreach (node_type_get_types() as $bundle_name => $type_obj) {
// Get the comment alter fields directly, rather than via
// comment_alter_get_alterable_fields(), because it will remove the fields
// which are on the comment.
$field_infos = field_info_instances('node', $bundle_name);
$comment_alter_fields = array();
foreach ($field_infos as $field_name => $value) {
if (!empty($value['comment_alter'])) {
$comment_alter_fields[] = $field_name;
}
}
if (!empty($comment_alter_fields)) {
$comment_fields = field_info_instances('comment', 'comment_node_' . $bundle_name);
foreach ($comment_fields as $field_name => $field_instance) {
if (in_array($field_name, $comment_alter_fields)) {
$requirements[] = array(
'title' => t('Comment alter'),
'value' => t('Invalid fields error'),
'description' => t('The %field_name field exists on the comment and is alterable from comment on the parent node. Please remove it from the comment or disable altering it from comment!', array(
'%field_name' => $field_name,
)),
'severity' => REQUIREMENT_ERROR,
);
}
}
}
}
}
return $requirements;
}
/**
* Implements hook_form_BASE_FORM_ID_alter().
*
* @see _comment_alter_submit_node_fields()
*/
function comment_alter_form_comment_form_alter(&$form, &$form_state, $form_id) {
// Check if we are on the reply-to-comment form and if it should contain
// comment_alterable fields at all (bail out early if needed).
if ($form['pid']['#value'] && variable_get('comment_alter_root_only_' . $form['#node']->type, 0)) {
return;
}
// Load the latest revision instead of the current if asked for.
if (variable_get('comment_alter_' . $form['#node']->type, 0)) {
$vid = db_select('node_revision', 'nr')
->fields('nr', array(
'vid',
))
->condition('nid', $form['#node']->nid)
->orderBy('timestamp', 'DESC')
->range(0, 1)
->execute()
->fetchField();
}
else {
$vid = NULL;
}
$node = node_load($form['#node']->nid, $vid);
$comment_alter_fields = comment_alter_get_alterable_fields($node->type);
// Add widgets to comment form if comment form is new, and if comment alter
// fields are available.
if (empty($form['cid']['#value']) && !empty($comment_alter_fields)) {
// Every comment alter field goes to the root of the comment form so this
// enables that other modules can re-order or group the fields.
$field_infos = field_info_extra_fields('comment', 'comment_node_' . $node->type, 'form');
// The _comment_alter_submit_node_fields() function needs these two arrays.
// This one is a list of comment_alterable fields.
$alterable_fields = array();
// This one informs about comment_alterable columns per comment_alterable
// fields. First-level key is the field name, second-level keys (and values)
// are the columns which do have their form elements.
$alterable_columns = array();
foreach ($comment_alter_fields as $field_name) {
// Attach the node field to the comment form. We do it on a copy of the
// $form, so that we know we are only getting the field element itself and
// no other side-effects (for example, the field_groups from the node
// entity).
$node_form = $form;
field_attach_form('node', $node, $node_form, $form_state, $node->language, array(
'field_name' => $field_name,
));
if (!empty($node_form[$field_name])) {
$form[$field_name] = $node_form[$field_name];
$field_language = $form[$field_name]['#language'];
$field_value = array();
// Not sure that there is a language key in the field if the field value
// is empty.
if (!empty($node->{$field_name}[$field_language])) {
$field_value[$field_language] = $node->{$field_name}[$field_language];
}
// Comment_alterable node fields must be amongst the comment fields
// themselves in order to be able to properly weight/reorder them, put
// them into a field_group, etc. on the manage comment fields form.
$form[$field_name . '_old'] = array(
'#type' => 'value',
'#value' => $field_value,
);
// TODO: This doesn't seem to be working properly: if the extra field is
// set to last, it even gets displayed above comment subject.
$form[$field_name]['#weight'] = $field_infos[$field_name]['weight'];
// Remember that this field is alterable.
$alterable_fields[$field_name] = $field_name;
// Fetch the alterable columns from field items themselves. If the
// #column info is available at the top-level (eg. in the case of a
// select), grab it from there; in other cases, grab it from the first
// item.
// Note: weight changes (changes in order of multivalue fields' values)
// are not tracked.
$field_items = $form[$field_name][$field_language];
if (isset($field_items['#columns'])) {
$columns = $field_items['#columns'];
}
else {
$field_item = $field_items[0];
if (isset($field_item['#columns'])) {
$columns = $field_item['#columns'];
}
else {
$field_item = reset($field_item);
$columns = $field_item['#columns'];
}
}
foreach ($columns as $column) {
$alterable_columns[$field_name][$column] = $column;
}
}
}
if (!empty($alterable_fields)) {
// Push down the information gathered so far to
// _comment_alter_submit_node_fields().
$form['comment_alter'] = array(
'#type' => 'value',
'#value' => array(
'fields' => $alterable_fields,
'old_vid' => $node->vid,
'alterable_columns' => $alterable_columns,
),
);
// Put the node entity in $form_state so can get it in submit/validate
// without loading it again.
$form_state['node'] = $node;
// Add submit/validate function so we can call
// field_attach_form_validate() and field_attach_submit().
$form['#validate'][] = '_comment_alter_validate_node_fields';
$form['#submit'][] = '_comment_alter_submit_node_fields';
}
}
}
/**
* Helper function to clean up field values for comparison.
*
* Removes 'add_more' and non-alterable columns (like _weight) so we can compare
* the old and new field values and find changes.
*
* @param array $values
* Array whose elements should be cleaned up.
* @param string $field_name
* Clean up this field of the array.
* @param string $old = ''
* Suffix for old value cleanup.
*/
function _comment_alter_cleanup_field_values(&$values, $field_name, $old = '') {
$field_value =& $values[$field_name . $old];
$alterable_columns = $values['comment_alter']['alterable_columns'][$field_name];
// Multiple-value fields should not have an 'add_more' delta (this comes from
// the Field API's Form API stuff: 'add_more' delta is the 'Add more' button.
foreach ($field_value as $language => $deltas) {
foreach ($field_value[$language] as $delta => $columns) {
if ($delta === 'add_more') {
unset($field_value[$language][$delta]);
continue;
}
// Non-alterable columns (eg. _weight) should be removed (from
// comparison).
foreach ($field_value[$language][$delta] as $column => $value) {
if (!isset($alterable_columns[$column])) {
unset($field_value[$language][$delta][$column]);
}
}
}
}
_comment_alter_cleanup_arrays($field_value);
}
/**
* Helper function to recursively clean up semi-empty arrays.
*
* Eg. array('foo' => array('bar' => array('baz' => ''))) becomes array().
*
* @param array $a
* Array whose empty elements should be removed.
*/
function _comment_alter_cleanup_arrays(&$a) {
if (is_array($a)) {
foreach ($a as $key => &$value) {
if (is_array($value)) {
_comment_alter_cleanup_arrays($value);
}
if (empty($value)) {
unset($a[$key]);
}
}
}
}
/**
* Validation callback for the altered comment form.
*
* Calls form_attach_form_validate() for each of the alterable node fields.
*
* @see form_attach_form_validate()
* @see comment_alter_form_comment_form_alter()
*/
function _comment_alter_validate_node_fields($form, &$form_state) {
$node = clone $form_state['node'];
foreach ($form_state['values']['comment_alter']['fields'] as $field_name) {
field_attach_form_validate('node', $node, $form, $form_state, array(
'field_name' => $field_name,
));
}
}
/**
* Submit callback for the altered comment form.
*
* Determines which fields have actually changed, then calls
* form_attach_submit() on each of them, and saves the resulting node.
*
* @see form_attach_submit()
* @see comment_alter_form_comment_form_alter()
*/
function _comment_alter_submit_node_fields($form, &$form_state) {
$values = $form_state['values'];
// Do not try to save anything if there is nothing that was allowed to be
// changed from the comment form.
if (isset($values['comment_alter'])) {
$changed_fields = array();
foreach ($values['comment_alter']['fields'] as $field_name) {
_comment_alter_cleanup_field_values($values, $field_name);
_comment_alter_cleanup_field_values($values, $field_name, '_old');
// If field values have changed, add it to the list.
if ($values[$field_name . '_old'] != $values[$field_name]) {
$changed_fields[$field_name] = $field_name;
}
}
if (!empty($changed_fields)) {
$node = $form_state['node'];
// Run field_attach_submit for all the changed fields.
foreach ($form_state['values']['comment_alter']['fields'] as $field_name) {
field_attach_submit('node', $node, $form, $form_state, array(
'field_name' => $field_name,
));
}
// Special support for the Title field via the Title module.
if (module_exists('title') && isset($form_state['values']['comment_alter']['fields']['title_field'])) {
// Calling this should be enough:
// title_entity_sync('node', $node, $node->language);
// But it isn't. That function uses a static variable to make sure it is
// only run once per request, however, under certain conditions observed
// when using Panels, title_entity_sync() can be called several times
// before ever reaching this point. So, the following code was copied
// from title_entity_sync() to avoid the static variable.
$fr_info = title_field_replacement_info('node');
if ($fr_info) {
foreach ($fr_info as $legacy_field => $info) {
if (title_field_replacement_enabled('node', $node->type, $legacy_field)) {
title_field_sync_get('node', $node, $legacy_field, $info, $node->language);
}
}
}
}
// Creating a new node revision regardless the node type settings.
$node->revision = TRUE;
// Disable node updated notifications that caused by comment_alter. New
// comment notifications should be enough - this happens when submitting a
// comment, not when editing a node, after all. This disabling is
// unconditional: there may be other modules than notifications_content
// that wants these notifications be disabled; reusing the same key seems
// to be a non-issue here as it won't get stored anywhere.
$node->notifications_content_disable = TRUE;
node_save($node);
// Fire the comment_alter_node_postsave hook with the $node and $comment
// objects; though $node does not have a $node->original property, all the
// affected fields' information (both previous and current values) are
// available in the comment object.
$node->original = node_load($node->nid, $values['comment_alter']['old_vid']);
module_invoke_all('comment_alter_node_postsave', $node, $form_state['comment']);
$comment_alter = array(
'old_vid' => $values['comment_alter']['old_vid'],
'new_vid' => $node->vid,
'cid' => $values['cid'],
);
drupal_write_record('comment_alter', $comment_alter);
}
}
}
/**
* Implements hook_comment_load().
*/
function comment_alter_comment_load($comments) {
$result = db_select('comment_alter', 'ca')
->fields('ca', array(
'cid',
'old_vid',
'new_vid',
))
->condition('cid', array_keys($comments), 'IN')
->execute();
foreach ($result as $row) {
$comments[$row->cid]->comment_alter['old_vid'] = $row->old_vid;
$comments[$row->cid]->comment_alter['new_vid'] = $row->new_vid;
}
}
/**
* Returns the differences committed with a particular comment.
*
* Uses the 'Diff' module to actually generate the differences.
*
* @param object $comment
* The comment object.
* @param string $langcode
* The language code used for rendering the fields.
*
* @return array
* An associative array with keys being the changed field names and values
* being lists of associative arrays with 3 keys:
* - name: field's name being changed (only for the first field value).
* - old: array of old field values.
* - new: array of new field values.
*
* @see diff_compare_entities()
*/
function comment_alter_get_changed_fields($comment, $langcode) {
$changed_fields = array();
if (isset($comment->comment_alter) && isset($comment->comment_alter['new_vid'])) {
$node = node_load($comment->nid);
module_load_include('inc', 'diff', 'diff.pages');
$old_node = node_load($comment->nid, $comment->comment_alter['old_vid']);
$new_node = node_load($comment->nid, $comment->comment_alter['new_vid']);
$context = array(
'entity_type' => 'node',
'states' => array(
'raw',
),
'view_mode' => 'diff_standard',
'language' => $langcode,
);
$comment_alter_fields = comment_alter_get_alterable_fields($node->type);
// Because the 'title_field' is 'hidden' on all display modes, the Diff
// module will skip over it. However, the diff module has special support
// for the 'title' property, so we swap 'title_field' for 'title'.
if (($title_field_index = array_search('title_field', $comment_alter_fields)) !== FALSE) {
$comment_alter_fields[$title_field_index] = 'title';
}
$diffs = diff_compare_entities($old_node, $new_node, $context);
foreach ($diffs as $field_name => $diff) {
// Only compare fields that belong to us.
$field = field_info_field($field_name);
if (field_access('view', $field, 'node') && in_array($field_name, $comment_alter_fields)) {
$instance = field_info_instance('node', $field_name, $node->type);
list($old, $new) = diff_extract_state($diff, $context['states'][0]);
if ($old != $new) {
if (!empty($instance['comment_alter_hide'])) {
if (diff_node_revision_access($node)) {
$changed_fields[$field_name][] = array(
'name' => $diff['#name'],
'changes' => l(t('View changes'), 'node/' . $node->nid . '/revisions/view/' . $old_node->vid . '/' . $new_node->vid),
);
}
else {
$changed_fields[$field_name][] = array(
'name' => $diff['#name'],
'old' => array(
'…',
),
'new' => array(
'…',
),
);
}
continue;
}
$rows = diff_get_rows($old, $new);
$line = 0;
foreach ($rows as $row) {
$changed_fields[$field_name][] = array(
'name' => $line ? '' : $diff['#name'],
'old' => array(
empty($row[1]['data']) ? '' : $row[1]['data'],
),
'new' => array(
empty($row[3]['data']) ? '' : $row[3]['data'],
),
);
$line++;
}
}
}
}
}
return $changed_fields;
}
/**
* Implements hook_comment_view().
*
* @see diff_diffs_show()
*/
function comment_alter_comment_view($comment, $view_mode, $langcode) {
// Strip 'comment_node_' to get the node type machine name.
$type = substr($comment->node_type, 13);
if ($view_mode == 'full' && isset($comment->comment_alter) && user_access('view comment alterations in ' . $type)) {
$comment->content['comment_alter'] = array(
'#theme' => 'comment_alter_diff',
'#changed_fields' => comment_alter_get_changed_fields($comment, $langcode),
'#comment' => $comment,
'#langcode' => $langcode,
);
}
// Add the link if needed AND current user has access to it.
if (isset($comment->comment_alter) && isset($comment->comment_alter['new_vid'])) {
$path = 'node/' . $comment->nid . '/revisions/view/' . $comment->comment_alter['old_vid'] . '/' . $comment->comment_alter['new_vid'];
if (variable_get('comment_alter_diff_link_' . $type, 0) && drupal_valid_path($path)) {
$comment->content['links']['comment']['#links']['comment-alter'] = array(
'title' => t('diff'),
'href' => $path,
'html' => TRUE,
);
}
}
}
/**
* Implements hook_theme().
*/
function comment_alter_theme() {
$return = array();
$return['comment_alter_diff'] = array(
'variables' => array(
'changed_fields' => array(),
'comment' => NULL,
'langcode' => NULL,
),
);
return $return;
}
/**
* Returns HTML for changes made by comment_alter.
*
* @param array $variables
* An associative array containing:
* - changed_fields: a list of arrays of changed fields, with these indexes:
* - name: field's name being changed.
* - old: array of old field values.
* - new: array of new field values.
* - comment: Full comment object, for context.
* - langcode: The language code used for rendering the fields, for context.
*
* @ingroup themeable
*/
function theme_comment_alter_diff(&$variables) {
if ($variables['changed_fields']) {
$rows = array();
foreach ($variables['changed_fields'] as $field) {
foreach ($field as $value) {
$row = array(
empty($value['name']) ? '' : $value['name'] . ':',
);
if (!empty($value['changes'])) {
$row[] = array(
'data' => $value['changes'],
'colspan' => 3,
);
}
else {
$row[] = implode(', ', $value['old']);
$row[] = '»';
$row[] = implode(', ', $value['new']);
}
$rows[] = $row;
}
}
drupal_add_css(drupal_get_path('module', 'comment_alter') . '/comment_alter.css');
$build = array(
'#theme' => 'table__comment_alter__diff',
'#rows' => $rows,
'#attributes' => array(
'class' => array(
'comment-alter-diff',
),
),
'#sticky' => FALSE,
);
return drupal_render($build);
}
}
/**
* Implements hook_comment_delete().
*/
function comment_alter_comment_delete($comment) {
db_delete('comment_alter')
->condition('cid', $comment->cid)
->execute();
}
/**
* No need to implement hook_comment_update(), hook_comment_presave(),
* hook_comment_publish(), hook_comment_unpublish() nor
* hook_comment_view_alter().
*/
/**
* Implements hook_field_extra_fields().
*/
function comment_alter_field_extra_fields() {
$return = array();
foreach (node_type_get_types() as $type) {
$comment_alter_fields = comment_alter_get_alterable_fields($type->type);
$weight = 0;
foreach ($comment_alter_fields as $field_name) {
$field = field_info_instance('node', $field_name, $type->type);
// Comment_alterable node fields must be amongst the comment fields
// themselves in order to be able to properly weight/reorder them, put
// them into a field_group, etc. on the manage comment fields form.
$return['comment']['comment_node_' . $type->type]['form'][$field_name] = array(
'label' => $field['label'],
'description' => $field['description'],
'weight' => $weight,
);
$weight++;
}
if (!empty($comment_alter_fields)) {
$return['comment']['comment_node_' . $type->type]['display']['comment_alter'] = array(
'label' => t('Comment changes'),
'description' => t('Changes made to the parent node\'s fields in this comment.'),
'weight' => -1,
);
}
}
return $return;
}
/**
* Implements hook_permission().
*/
function comment_alter_permission() {
$return = array();
foreach (node_type_get_types() as $type) {
$return['view comment alterations in ' . $type->type] = array(
// TODO: I18n support.
'title' => t('View comment alterations of %type nodes', array(
'%type' => $type->name,
)),
);
}
return $return;
}
/**
* Implements hook_node_presave().
*/
function comment_alter_node_presave($node) {
if (!isset($node->notifications_content_disable) && module_exists('rules')) {
rules_invoke_event('comment_alter_node_presave', $node);
}
}
/**
* Implements hook_node_update().
*/
function comment_alter_node_update($node) {
if (!isset($node->notifications_content_disable) && module_exists('rules')) {
rules_invoke_event('comment_alter_node_update', $node);
}
}
/**
* Implements hook_comment_alter_node_postsave().
*/
function comment_alter_comment_alter_node_postsave($node, $comment) {
if (module_exists('rules')) {
rules_invoke_event('comment_alter_node_postsave', $node, $comment);
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function comment_alter_form_node_type_form_alter(&$form, &$form_state, $form_id) {
$node_type =& $form['#node_type']->type;
$form['comment']['comment_alter'] = array(
'#type' => 'checkbox',
'#title' => t('Use the latest revision as the default values for comment alterable fields (if any)'),
'#description' => t('If you want to load the field values from the latest revision of the node (instead of the current revision) when the comment form is displayed (eg. when the <a href="!url">Revisioning</a> module is being used), tick this.', array(
'!url' => 'https://drupal.org/project/revisioning',
)),
'#default_value' => variable_get('comment_alter_' . $node_type, 0),
);
$form['comment']['comment_alter_diff_link'] = array(
'#type' => 'checkbox',
'#title' => t('Add a link to the usual diff of the two revisions to the links area of the comment'),
'#default_value' => variable_get('comment_alter_diff_link_' . $node_type, 0),
);
$form['comment']['comment_alter_root_only'] = array(
'#type' => 'checkbox',
'#title' => t('Allow altering fields only when submitting root-level comments'),
'#description' => t('Ie. disallow comment altering when replying to comments.'),
'#default_value' => variable_get('comment_alter_root_only_' . $node_type, 0),
'#states' => array(
'visible' => array(
// action to take.
':input[name=comment_default_mode]' => array(
'checked' => TRUE,
),
),
),
);
}
/**
* Implements hook_node_revision_delete().
*/
function comment_alter_node_revision_delete($node) {
db_delete('comment_alter')
->condition(db_or()
->condition('old_vid', $node->vid)
->condition('new_vid', $node->vid))
->execute();
}