Skip to main content

Custom CCK field for Drupal 6

Posted in

About this module

This is a Drupal 6 Module which serves the purpose of enabling a custom CCK field for web server ssl certificate creation. Although it works, it is meant to be an example only. It shows:

  • Creation of a custom CCK field, consisting of multiple textarea type fields
  • Filling the fields with the output of a custom function, wich gets
  • triggered by a button which is in the CCK field itself
  • AHAH (Ajax) is used to update the custom field without reloading the form

The source code has grown from the demand of a ssl cert group field. During the learning process, some other modules were taken as a template, modified, extended by functions from other modules, and so on. I hope it is ok for the authors that some comments from their source code are included. The source code is, hopefully, well commented, so you should get an idea on how the parts match together. Some pitfalls have been circumvented, but one issue is left:

The field “cert_key” should and does not need to be transfered to the client. If the AHAH handling is not active, this field may be set to '#type' ⇒ 'value', this prevents Drupal from sending the key over the wire, keeping it safe on the host system itself. Everithyng works nice, but the complete form is reloaded upon creation of the key/certificate request.

But if AHAH is active, a 'value' type field can not be changed any more, so one has to set set it to either 'hidden' or 'textara'. In case someone knows a solution for this problem, please let me know. I tried for abt. 6 hours before giving up on this.

This is the field element involved:

$element['cert_key'] = array(
    '#input' => TRUE,
    '#title' => t('Private Key'),
    '#type' => 'hidden',
    //'#type' => 'textarea',
    //'#type' => 'value',
    '#default_value' => isset($items[$delta]['cert_key']) ? $items[$delta]['cert_key'] : NULL,
    '#weight' => 3,
    );

Installation

If you would like to play arround with this module:

  • Download the module from gitweb (choose the “snapshot” link)
  • put the module files to sites/all/modules/sslcert_fld
  • Enable the module
  • Make shure openssl is installed on your server. This was tested on Linux only
  • For a short test only, create a new content type, and add the field type “SSL Certificate” to your content type
  • From the new content type, create a node. If you create a certificate request, make shure the field “Fully Qualified Domain Name” contains a string.

References

The following articles were used during the building process:

Don't forget to read the source code of your Drupal installation. The filefield module source could give some very useful information.

The Source Code

<?php
// $Id:$
 
/**
 * @file
 * This is a Drupal Module - Server cert
 * It implements a server cert field for CCK
 *
 * Author: Erik Beckers, DL2KEB
 *
 * Licensed under the GNU General Public License
 *
 * This module is heavily based on the person_fld module
 * Copyright 2009 Jennifer Hodgdon, Poplar ProductivityWare LLC
 * http://poplarware.com/articles/cck_field_module
 * 
 * and the file_field module from Drupal 6 (AHAH)
 *
 * See also: http://www.lullabot.com/articles/creating-custom-cck-fields (very good! 
 * fields defined in hook_widget instead of _process, making 'type' => 'value' fields work),
 * and http://www.trellon.com/content/blog/cck-creating-new-field-types
 */
 
/**
 * Implementation of CCK's hook_field_info().
 *
 * Returns basic information about this CCK field (module section).
 */
 
function sslcert_fld_field_info() {
  return array(
      'certificate' => array(
	'label' => t('Server Certificate'),
	'description' => t('Stores an SSL Server Certificate'),
	)
      );
}
/**
 * Implementation of CCK hook_field_settings().
 */
function sslcert_fld_field_settings($op, $field) {
  switch ($op) {
    case 'database columns':
      $columns['cert_fqdn'] = array(
	  'type' => 'varchar', 
	  'length' => 255, 
	  'not null' => FALSE, 
	  'sortable' => TRUE, 
	  'default' => '');
      $columns['cert_key'] = array(
	  'type' => 'text', 
	  'length' => 4096, 
	  'not null' => FALSE, 
	  'sortable' => FALSE, 
	  'default' => '');
      $columns['cert_csr'] = array(
	  'type' => 'text', 
	  'length' => 4096, 
	  'not null' => FALSE, 
	  'sortable' => FALSE, 
	  'default' => '');
      $columns['cert_crt'] = array(
	  'type' => 'text', 
	  'length' => 4096, 
	  'not null' => FALSE, 
	  'sortable' => FALSE, 
	  'default' => '');
      return $columns;
      /* this will be useful for modules which use/extend views, but atm no 
	 clue how it works. Later...:
	 case 'views data':
	 $allowed_values = content_allowed_values($field);
	 if (count($allowed_values)) {
	 $data = content_views_field_views_data($field);
	 $db_info = content_database_info($field);
	 $table_alias = content_views_tablename($field);
	 $field_data = $data[$table_alias][$field['field_name'] .'_value'];
	 return $data;
	 }
       */
  }
}
/**
 * Implementation of hook_widget_settings().
 *
 * Create the form element to be used on the widget settings form. Widget settings 
 * can be different for each shared instance of the same field and should define 
 * the way the value is displayed to the user in the edit form for that content type.
 */
function sslcert_fld_widget_settings($op, $widget) {
  switch ($op) {
    case 'form':
      $form = array();
      $extensions = is_string($widget['file_extensions']) ? $widget['file_extensions'] : 'txt';
      $extensions = implode(', ', explode(' ', $extensions));
      $form['details'] = array(
	  '#type' => 'fieldset',
	  '#title' => t('Header'),
	  '#description' => t('Use these settings to change the field properties. These settings have no effect.'),
	  '#collapsible' => TRUE,
	  '#collapsed' => FALSE,
	  );
      $form['details']['fqdnfield'] = array(
	  '#type' => 'textfield',
	  '#title' => t('CCK field that contains the Fully Qualified Domain Name'),
	  // set the default from the widget value, else nothing will show up.
	  '#default_value' => is_string($widget['fqdnfield']) ? $widget['fqdnfield'] : '',
	  '#size' => 64,
	  '#description' => t('CCK field that contains the Fully Qualified Domain Name. 
	    If you would like this field to be used, implement it in _sslcert_fld_button_action'),
	  '#weight' => 2,
	  );
      return $form;
 
      // Return an array of the names of the widget settings
      // defined by this module. These are the items that
      // CCK will store in the widget definition and they
      // will be available in the $field['widget'] array.
      // This should match the items defined in 'form' above.
    case 'save':
      return array('fqdnfield', 'file_extensions');
    case 'callbacks':
      return array(
	  'default value' => CONTENT_CALLBACK_NONE,
	  );
 
  }
}
/**
 * Implementation of hook_content_is_empty().
 * Enables CCK to find out if the field/field set is "empty"
 */
function sslcert_fld_content_is_empty($item, $field) {
  if (empty($item['cert_fqdn'])) {
    return TRUE;
  }
  return FALSE;
}
/**
 * Implementation of hook_widget_info().
 */
function sslcert_fld_widget_info() {
 
  return array(
      'certificate_entry' => array(
	'label' => t('Cert fields'),
	'field types' => array('certificate'),
	'multiple values' => CONTENT_HANDLE_CORE,
	'callbacks' => array(
          /* we don't need a default value for this field in admin settings, 
           * so set this to CONTENT_CALLBACK_NONE
           * 'default value' => CONTENT_CALLBACK_DEFAULT,
           */
	  'default value' => CONTENT_CALLBACK_NONE,
	  ),
	),
      );
}
/**
 * Implementation of Form API's hook_elements().
 *
 * Returns a skeleton Form API array that defines callbacks
 * for the widget form.
 */
function sslcert_fld_elements() {
  $elements = array('certificate_entry' =>
      array(
	'#input' => TRUE,
	// valid processing point for values, see http://drupal.org/node/169815
	'#process' => array('sslcert_fld_certificate_entry_process'),
	),
      );
 
  return $elements;
}
/**
 * Process callback for widget
 *
 * Returns a Forms API array that defines the widget's editing form.
 *
 * Valid processing point for values, in case it is defined
 * as #process in hook_elements, see http://drupal.org/node/169815
 *
 * Do *NOT* define 'type' => 'value' fields here, they will simply *NOT WORK*
 *
 * The #process callback gets four arguments, $element, $edit, $form_state and 
 * $complete_form, and expects to have $element returned. This is the last place 
 * you can override the default value. By this point in the processing, 
 * #default_value has been moved to #value in the element, so to change the 
 * value in here you need to alter $element['#value'], not 
 * $element['#default_value'].
 */
function sslcert_fld_certificate_entry_process($element, $edit, &$form_state, $form) {
 
  /* grab the url from a different CCK field in the same form. This gets not transfered
   * to the cert creation function, if you need this, add it accordingly.
   */
  $url_field = 'url';
  $url_field_name = 'field_'. $url_field;
  $url_value = $form_state['values']['field_'. $url_field][0]['value'];
 
  $element['create_cert'] = array(
      '#name' => implode('_', $element['#parents']) .'_create_cert',
      '#type' => 'submit',
      '#value' => t('Create Cert'),
      '#submit' => array('node_form_submit_build_node'),
      '#ahah' => array( // with JavaScript
	'path' => 'sslcert_fld/ahah/'.   $element['#type_name'] .'/'. $element['#field_name'] .'/'. $element['#delta'],
	'wrapper' => $element['#id'] .'-ahah-wrapper',
	'method' => 'replace',
	'effect' => 'fade',
	),
      '#field_name' => $element['#field_name'],
      '#delta' => $element['#delta'],
      '#weight' => 101,
      '#post' => $element['#post'],
      );
 
  // Check if the button for 'create_cert' was pressed, and react accordingly.
  _sslcert_fld_button_action(&$element, 'create_cert', &$form_state);
 
  // define prefix/suffix for AHAH processing
  $element['#attributes']['id'] = $element['#id'] .'-ahah-wrapper';
  $element['#prefix'] = '<div '. drupal_attributes($element['#attributes']) .'>';
  $element['#suffix'] = '</div>';
 
  return $element;
}
function _sslcert_fld_button_action(&$element, $button, &$form_state){
  switch ($button) {
    case 'create_cert':
      if (_form_button_was_clicked($element['create_cert'])) {
	$defaults = $element['#value'];
 
	$configargs = array(
	    'config' => '/etc/ssl/openssl.cnf',
	    'digest_alg' => 'sha1',
	    'private_key_bits' => 2048,
	    'private_key_type' => OPENSSL_KEYTYPE_RSA,
	    'encrypt_key' => false,
	    );
	// adjust this to your needs, or implement a settings field.
	$dn = array(
	    "countryName" => "DE",
	    "stateOrProvinceName" => "NRW",
	    "localityName" => "Western",
	    "organizationName" => "DL2KEB",
	    "organizationalUnitName" => "Erik",
	    "commonName" => $defaults['cert_fqdn'],
	    );
 
	$privkey = null;
	// Generate a certificate signing request. Key is generated in one step.
	$csr = openssl_csr_new($dn, $privkey, $configargs);
 
	openssl_csr_export($csr, $csrout);
	$element['cert_csr']['#value']=$csrout;
	openssl_pkey_export($privkey, $pkeyout, '', $configargs);
	$element['cert_key']['#value']=$pkeyout;
	drupal_set_message('The request has not been saved yet. Please press save before continuing.');
      }
      break;
  }
}
/**
 * Implementation of hook_widget().
 *
 * Attach form element(s) to the form.
 *
 * CCK core fields only add a stub element and build
 * the complete item in #process so reusable elements
 * created by hook_elements can be plugged into any
 * module that provides valid $field information.
 *
 * BUT: Do define 'type' => 'value' fields in this function, else they will simply *NOT WORK*
 *
 * Custom widgets that don't care about using hook_elements
 * can be built out completely at this time.
 *
 * If there are multiple values for this field and CCK is
 * handling multiple values, the content module will call
 * this function as many times as needed.
 *
 * @param $form
 *   the entire form array,
 *   $form['#node'] holds node information
 * @param $form_state
 *   the form_state,
 *   $form_state['values'][$field['field_name']]
 *   holds the field's form values.
 * @param $field
 *   the field array
 * @param $items
 *   array of default values for this field
 * @param $delta
 *   the order of this item in the array of
 *   subelements (0, 1, 2, etc)
 *
 * @return
 *   the form item for a single element for this field
 */
function sslcert_fld_widget(&$form, &$form_state, $field, $items, $delta = 0) {
  $element = array(
      '#type' => $field['widget']['type'],
      '#default_value' => isset($items[$delta]) ? $items[$delta] : '',
      );
 
  /**
   * Label and help text from the cck field settings go here, giving an opportunity
   * to clarify something during field creation.
   */
  $element['label'] = array(
      '#type' => 'item',
      '#title' => !empty($field['widget']['label']) ? $field['widget']['label'] : '',
      '#value' => !empty($field['widget']['description']) ? $field['widget']['description'] : '',
      );
 
  $element['cert_fqdn'] = array(
      '#title' => t('Fully Qualified Domain Name'),
      '#type' => 'textfield',
      '#default_value' => isset($items[$delta]['cert_fqdn']) ? $items[$delta]['cert_fqdn'] : NULL,
      '#weight' => 2,
      );
  /**
   * The field "cert_key" should and does not need to be transfered to the client. If
   * the AHAH handling is not active, this field may be set to '#type' => 'value',
   * this prevents Drupal from sending the key over the wire, keeping it safe on the
   * host system itself.
   * If AHAH is active, a 'value' type field can not be changed any more, so set 
   * it to either 'hidden' or 'textara'. In case someone knows a solution for this
   * problem, please let me know. I tried for abt. 6 hours...
   */
  $element['cert_key'] = array(
      '#input' => TRUE,
      '#title' => t('Private Key'),
      '#type' => 'hidden',
      //'#type' => 'textarea',
      //'#type' => 'value',
      '#default_value' => isset($items[$delta]['cert_key']) ? $items[$delta]['cert_key'] : NULL,
      //'#value' => isset($items[$delta]['cert_key']) ? $items[$delta]['cert_key'] : NULL,
      '#weight' => 3,
      );
  $element['cert_csr'] = array(
      '#title' => t('Certificate Signing Request'),
      '#type' => 'textarea',
      '#default_value' => isset($items[$delta]['cert_csr']) ? $items[$delta]['cert_csr'] : NULL,
      '#weight' => 4,
      );
  // show the cert_crt field only if the other fields are not empty
  if( (!empty($items[$delta]['cert_fqdn'])) && 
      (!empty($items[$delta]['cert_key'])) && 
      (!empty($items[$delta]['cert_csr'])) ) {
    $element['cert_crt'] = array(
	'#title' => t('Server Certificate'),
	'#type' => 'textarea',
	'#default_value' => isset($items[$delta]['cert_crt']) ? $items[$delta]['cert_crt'] : NULL,
	'#weight' => 5,
	);
  }
  else {
    $element['cert_crt'] = array(
	'#type' => 'hidden',
	'#default_value' => isset($items[$delta]['cert_crt']) ? $items[$delta]['cert_crt'] : NULL,
	'#weight' => 5,
	);
  }
  // Used so that hook_field('validate') knows where to
  // flag an error in deeply nested forms.
  if (empty($form['#parents'])) {
    $form['#parents'] = array();
  }
  $element['_error_element'] = array(
      '#type' => 'value',
      '#value' => implode('][', array_merge($form['#parents'], array('value'))),
      );
 
  return $element;
}
/**
 * Implementation of CCK's hook_field_formatter_info().
 *
 * Returns information about available field formatters.
 * Only formatters used in node view and views need to be defined here.
 * 
 */
function sslcert_fld_field_formatter_info() {
  return array(
      'default' => array(
	'label' => t('Extended display'),
	'field types' => array('certificate'),
	),
      'short' => array(
	'label' => t('Status display'),
	'field types' => array('certificate'),
	),
      );
}
/**
 * Implementation of hook_theme().
 * all "views" have to be defined here: The edit view
 * as well as node view and views views, which get themed.
 *
 */
function sslcert_fld_theme() {
  return array(
      // needed for the edit form
      'certificate_entry' => array(
	'arguments' => array('element' => NULL),
	),
      // needed for "Extended display" of node element
      'sslcert_fld_formatter_default' => array(
	'arguments' => array('element' => NULL),
	),
      // needed for "Short display" of node element
      'sslcert_fld_formatter_short' => array(
	'arguments' => array('element' => NULL),
	),
      );
}
/**
 * FAPI theme for an individual text elements.
 * This is used inside the edit form.
 */
function theme_certificate_entry($element) {
  return $element['#children'];
}
/**
 * Theme function for short formatter, used during display of "Short Display.
 */
function theme_sslcert_fld_formatter_short($element) {
  //Formatters obtain #item, not ['FIELD']['#value']
  $defaults = $element['#item'];
  $ret = '<div class="cert_info">';
  // "empty" is not null, it's empty string.
  if( (!empty($defaults['cert_fqdn'])) && (!empty($defaults['cert_key'])) && (!empty($defaults['cert_csr'])) ) {
    $ret .= 'Certificate created for '. $defaults['cert_fqdn'] .'<br />'; 
  }
  else {
    $ret .= 'No SSL Certificate created<br />'; 
  };
  $ret .= '</div>';
  return $ret;
}
/**
 * Theme function for default formatter, used during display of "Extended Display".
 */
function theme_sslcert_fld_formatter_default($element = NULL) {
  if(empty($element['#item'])) {
    return '';
  }
 
  $ret = '<div class="staff_info">';
  $sep = '';
  $defaults = $element['#item'];
  //kpr($defaults);
  if(!empty($defaults['cert_fqdn'])) {
    $ret .= 'Cert created for '. $defaults['cert_fqdn'] .'<br />'; 
  }
  else {
    $ret .= 'No cert created yet<br />'; 
  };
 
  $flds = array('cert_fqdn', 'cert_key', 'cert_csr', 'cert_crt');
  foreach($flds as $fld) {
    if(!empty($defaults[ $fld ])) {
      $ret .= $sep . '<pre class="' . $fld . '">'. $fld .': ' . $defaults[ $fld ] . '</pre>';
      $sep = "<br />\n";
    }
  }
 
  $ret .= '</div>';
 
  return $ret;
}
// AHAH, taken from filefield module
 
/**
 * Implementation of hook_menu().
 */
function sslcert_fld_menu() {
  $items = array();
 
  $items['sslcert_fld/ahah/%/%/%'] = array(
      'page callback' => 'sslcert_fld_js',
      'page arguments' => array(2, 3, 4),
      'access callback' => 'sslcert_fld_edit_access',
      'access arguments' => array(2, 3),
      'type' => MENU_CALLBACK,
      );
 
  return $items;
}
/**
 * Access callback for the JavaScript AHAH callbacks.
 *
 * The content_permissions module provides nice fine-grained permissions for
 * us to check, so we can make sure that the user may actually edit the file.
 */
function sslcert_fld_edit_access($type_name, $field_name) {
  if (!content_access('edit', content_fields($field_name, $type_name))) {
    return FALSE;
  }
  // No content permissions to check, so let's fall back to a more general permission.
  return user_access('access content') || user_access('administer nodes');
}
/**
 * Menu callback; AHAH callback 
 *
 * This rebuilds the form element for a particular field item. As long as the
 * form processing is properly encapsulated in the widget element the form
 * should rebuild correctly using FAPI without the need for additional callbacks
 * or processing.
 */
function sslcert_fld_js($type_name, $field_name, $delta) {
  $field = content_fields($field_name, $type_name);
 
  // Immediately disable devel shutdown functions so that it doesn't botch our
  // JSON output.
  $GLOBALS['devel_shutdown'] = FALSE;
 
  if (empty($field) || empty($_POST['form_build_id'])) {
    // Invalid request.
    drupal_set_message(t('An unrecoverable error occurred.'), 'error');
    print drupal_to_js(array('data' => theme('status_messages')));
    exit;
  }
 
  // Build the new form.
  $form_state = array('submitted' => FALSE);
  $form_build_id = $_POST['form_build_id'];
  $form = form_get_cache($form_build_id, $form_state);
 
  if (!$form) {
    // Invalid form_build_id.
    drupal_set_message(t('An unrecoverable error occurred. This form was missing from the server cache. Try reloading the page and submitting again.'), 'error');
    print drupal_to_js(array('data' => theme('status_messages')));
    exit;
  }
 
  // Build the form. Since this form is already marked as cached
  // (the #cache property is TRUE), the cache is updated automatically and we
  // don't need to call form_set_cache().
  $args = $form['#parameters'];
  $form_id = array_shift($args);
  $form['#post'] = $_POST;
  $form = form_builder($form_id, $form, $form_state);
 
  // Update the cached form with the new element at the right place in the form.
  if (module_exists('fieldgroup') && ($group_name = _fieldgroup_field_get_group($type_name, $field_name))) {
    if (isset($form['#multigroups']) && isset($form['#multigroups'][$group_name][$field_name])) {
      $form_element = $form[$group_name][$delta][$field_name];
    }
    else {
      $form_element = $form[$group_name][$field_name][$delta];
    }
  }
  else {
    $form_element = $form[$field_name][$delta];
  }
 
  if (isset($form_element['_weight'])) {
    unset($form_element['_weight']);
  }
 
  $output = drupal_render($form_element);
 
  // AHAH is not being nice to us and doesn't know the "other" button (that is,
  // "Create Cert") yet. Which in turn causes it not to attach AHAH behaviours 
  // after replacing the element. So we need to tell it first.
 
  // Loop through the JS settings and find the settings needed for our button.
  $javascript = drupal_add_js(NULL, NULL);
  $sslcert_fld_ahah_settings = array();
  if (isset($javascript['setting'])) {
    foreach ($javascript['setting'] as $settings) {
      if (isset($settings['ahah'])) {
	foreach ($settings['ahah'] as $id => $ahah_settings) {
	  if (strpos($id, 'create-cert')) {
	    $sslcert_fld_ahah_settings[$id] = $ahah_settings;
	  }
	}
	}
      }
    }
 
    // Add the AHAH settings needed for our new button.
    if (!empty($sslcert_fld_ahah_settings)) {
      $output .= '<script type="text/javascript">jQuery.extend(Drupal.settings.ahah, '. drupal_to_js($sslcert_fld_ahah_settings) .');</script>';
    }
 
    $output = theme('status_messages') . $output;
 
    drupal_json(array('status' => TRUE, 'data' => $output));
    exit;
  }