<?php
// $Id: content.module,v 1.56.2.20 2006/09/19 00:41:42 JonBob Exp $

/**
 * @file
 * Allows administrators to define new content types.
 */

define(CONTENT_DB_STORAGE_PER_FIELD, 0);
define(CONTENT_DB_STORAGE_PER_CONTENT_TYPE, 1);

if (module_exist('views')) {
  include_once(drupal_get_path('module', 'content') .'/content_views.inc');
}
if (module_exist('pathauto')) {
  include_once(drupal_get_path('module', 'content') .'/content_pathauto.inc');
}
include_once(drupal_get_path('module', 'content') .'/content_crud.inc');

/**
 * Implementation of hook_help().
 */
function content_help($section) {
  switch ($section) {
    case 'admin/modules#description':
      return t('<strong>CCK:</strong> Allows administrators to define new content types.');
  }

  if (strpos($section, 'node/add') === 0) {
    $types = content_types();
    foreach ($types as $type_name => $type) {
      if ($section == 'node/add#' . $type_name) {
        return t($types[$type_name]['description']);
      }
      if ($section == 'node/add/' . $type_name) {
        return t($types[$type_name]['help']);
      }
    }
  }
}

/**
 * Implementation of hook_perm().
 */
function content_perm() {
  $perms = array('administer content types');
  foreach (content_types() as $name => $type) {
    $perms[] = 'create '. $name .' content';
    $perms[] = 'edit own '. $name .' content';
  }
  return $perms;
}

/**
 * Implementation of hook_menu().
 */
function content_menu($may_cache) {
  $items = array();
  $access = user_access('administer content types');

  // Only include administrative callbacks if we are viewing an admin page.
  if (arg(0) == 'admin' && arg(1) == 'node' && arg(2) == 'types') {
    include_once(drupal_get_path('module', 'content') .'/content_admin.inc');
  }

  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/node/types',
      'title' => t('content types'),
      'callback' => '_content_admin_type_overview',
      'access' => $access,
    );
    $items[] = array(
      'path' => 'admin/node/types/list',
      'title' => t('list'),
      'type' => MENU_DEFAULT_LOCAL_TASK,
      'weight' => -10,
    );
    $items[] = array(
      'path' => 'admin/node/types/add',
      'title' => t('add content type'),
      'callback' => '_content_admin_type_edit',
      'access' => $access,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/node/types/fields',
      'title' => t('fields'),
      'callback' => '_content_admin_type_fields',
      'access' => $access,
      'type' => MENU_LOCAL_TASK,
    );

    $types = content_types();
    foreach ($types as $type_name => $type) {
      $items[] = array(
        'path' => 'node/add/'. $type_name,
        'title' => t($type['label']),
        'access' => user_access('create '. $type_name .' content'),
      );
    }
  }
  else {
    if (arg(0) == 'admin' && arg(1) == 'node' && arg(2) == 'types' && arg(3)) {

      $type = content_types(arg(3));

      if ($type) {
        $items[] = array(
          'path' => 'admin/node/types/'. arg(3),
          'title' => t($type['label']),
          'callback' => '_content_admin_type_edit',
          'access' => $access,
          'callback arguments' => array(arg(3)),
          'type' => MENU_CALLBACK,
        );
        $items[] = array(
          'path' => 'admin/node/types/'. arg(3) .'/delete',
          'title' => t('delete'),
          'callback' => '_content_admin_type_delete',
          'access' => $access,
          'callback arguments' => array(arg(3)),
          'type' => MENU_CALLBACK,
        );
        $items[] = array(
          'path' => 'admin/node/types/'. arg(3) .'/duplicate',
          'title' => t('duplicate'),
          'callback' => '_content_admin_type_edit',
          'access' => $access,
          'callback arguments' => array('', arg(3)),
          'type' => MENU_CALLBACK,
        );

        $items[] = array(
          'path' => 'admin/node/types/'. arg(3) .'/edit',
          'title' => t('edit'),
          'type' => MENU_DEFAULT_LOCAL_TASK,
          'weight' => -10,
        );

        $items[] = array(
          'path' => 'admin/node/types/'. arg(3) .'/fields',
          'title' => t('manage fields'),
          'callback' => '_content_admin_field_overview',
          'access' => $access,
          'callback arguments' => array(arg(3)),
          'type' => MENU_LOCAL_TASK,
          'weight' => 1,
        );
        $items[] = array(
          'path' => 'admin/node/types/'. arg(3) .'/add_field',
          'title' => t('add field'),
          'callback' => '_content_admin_field_add',
          'access' => $access,
          'callback arguments' => array(arg(3)),
          'type' => MENU_LOCAL_TASK,
          'weight' => 2,
        );

        if (arg(4) == 'fields' && arg(5)
            && isset($type['fields'][arg(5)])) {
          $items[] = array(
            'path' => 'admin/node/types/'. arg(3) .'/fields/'. arg(5),
            'title' => t($type['fields'][arg(5)]['widget']['label']),
            'callback' => '_content_admin_field',
            'access' => $access,
            'callback arguments' => array(arg(3), arg(5)),
            'type' => MENU_CALLBACK,
          );
          $items[] = array(
            'path' => 'admin/node/types/'. arg(3) .'/fields/'. arg(5) .'/remove',
            'title' => t('remove field'),
            'callback' => '_content_admin_field_remove',
            'access' => $access,
            'callback arguments' => array(arg(3), arg(5)),
            'type' => MENU_CALLBACK,
          );
        }

      }
    }
  }

  return $items;
}

/**
 * Implementation of hook_node_info().
 */
function content_node_info() {
  $types = array();
  foreach (content_types() as $name => $type) {
    $types[$name] = array('name' => t($type['label']), 'base' => 'content');
  }
  return $types;
}

/**
 * Implementation of hook_access().
 */
function content_access($op, $node) {
  global $user;
  $type = is_string($node) ? $node : (is_array($node) ? $node['type'] : $node->type);

  if ($op == 'create') {
    return user_access('create '. $type .' content');
  }

  if ($op == 'update' || $op == 'delete') {
    if (user_access('edit own '. $type .' content') && ($user->uid == $node->uid)) {
      return TRUE;
    }
  }
}

/**
 * Implementation of hook_load().
 *
 * When loading one of the content.module nodes, we need to let each field handle
 * its own loading. This can make for a number of queries in some cases, so we
 * cache the loaded object structure and invalidate it during the update process.
 */
function content_load($node) {
  $cid = 'content:'. $node->nid .':'. $node->vid;
  if ($cached = cache_get($cid)) {
    return unserialize($cached->data);
  }
  else {
    $default_additions = _content_field_invoke_default('load', $node);
    if ($default_additions) {
      foreach ($default_additions as $key => $value) {
        $node->$key = $value;
      }
    }
    $additions = _content_field_invoke('load', $node);
    if ($additions) {
      foreach ($additions as $key => $value) {
        $default_additions[$key] = $value;
      }
    }
    cache_set($cid, serialize($default_additions), CACHE_PERMANENT);
    return $default_additions;
  }
}

/**
 * Implementation of hook_form().
 *
 * Each field defines its own component of the content entry form, via its
 * chosen widget.
 */
function content_form(&$node, &$param) {
  $form = array();
  $type = content_types($node->type);

  // Set form parameters so we can accept file uploads.
  $form['#attributes'] = array("enctype" => "multipart/form-data");

  $form['title'] = array(
    '#type' => 'textfield',
    '#title' => t($type['title_label']),
    '#required' => TRUE,
    '#default_value' => $node->title,
  );

  _content_widget_invoke('prepare form values', $node);
  $form = array_merge($form, _content_widget_invoke('form', $node));

  return $form;
}

/**
 * Implementation of hook_validate().
 *
 * Both widgets and fields have a chance to raise error flags when a node is
 * being validated.
 */
function content_validate(&$node) {
  _content_widget_invoke('validate', $node);
  _content_widget_invoke('process form values', $node);
  _content_field_invoke('validate', $node);
  _content_field_invoke_default('validate', $node);
}

/**
 * Implementation of hook_submit().
 *
 * At submit time, the widget does whatever data massaging is necessary so that
 * the field has the content in the expected format and can commit the changes
 * to the database.
 */
function content_submit(&$node) {
  _content_widget_invoke('submit', $node);
  _content_widget_invoke('process form values', $node);
  _content_field_invoke('submit', $node);
  _content_field_invoke_default('submit', $node);
}

/**
 * Implementation of hook_insert().
 */
function content_insert(&$node) {
  _content_field_invoke('insert', $node);
  _content_field_invoke_default('insert', $node);
}

/**
 * Implementation of hook_update().
 */
function content_update(&$node) {
  _content_field_invoke('update', $node);
  _content_field_invoke_default('update', $node);
  cache_clear_all('content:'. $node->nid .':'. $node->vid);
}

/**
 * Implementation of hook_delete().
 */
function content_delete(&$node) {
  _content_field_invoke('delete', $node);
  _content_field_invoke_default('delete', $node);
  cache_clear_all('content:'. $node->nid);
}

/**
 * Implementation of hook_view().
 */
function content_view(&$node, $teaser = FALSE, $page = FALSE) {
  if ($node->in_preview) {
    _content_widget_invoke('process form values', $node);
  }
  $node->body = implode('', _content_field_invoke('view', $node, FALSE, $page));
  $node->teaser = implode('', _content_field_invoke('view', $node, TRUE, $page));
  $node->readmore = (strlen($node->teaser) < strlen($node->body));
}

/**
 * Implementation of hook_nodeapi().
 *
 * When a revision is deleted, make sure the appropriate cache item is cleared.
 */
function content_nodeapi(&$node, $op, $teaser, $page) {
  switch ($op) {
    case 'delete revision':
      if (content_types($node->type)) {
        _content_field_invoke('delete revision', $node);
        _content_field_invoke_default('delete revision', $node);
        cache_clear_all('content:'. $node->nid .':'. $node->vid);
      }
      break;
  }
}

/**
 * Implementation of hook_field(). Handles common field housekeeping.
 *
 * This implementation is special, as content.module does not define any field
 * types. Instead, this function gets called after the type-specific hook, and
 * takes care of the database interface for field types that do not choose to
 * do their own storage.
 */
function content_field($op, &$node, $field, &$node_field, $teaser, $page) {
  $db_info = content_database_info($field);

  switch ($op) {
    case 'load':
      $column_names = array();
      foreach ($db_info['columns'] as $column => $attributes) {
        $column_names[] = $attributes['column'] .' AS '. $column;
      }
      if ($field['multiple']) {
        $result = db_query('SELECT '. implode(', ', $column_names) .' FROM {'. $db_info['table'] .'} WHERE vid = %d ORDER BY delta', $node->vid);
        $values = array();
        while ($value = db_fetch_array($result)) {
          $values[] = $value;
        }
        $additions = array($field['field_name'] => $values);
      }
      else {
        $result = db_query('SELECT '. implode(', ', $column_names) .' FROM {'. $db_info['table'] .'} WHERE vid = %d', $node->vid);
        $additions = array($field['field_name'] => array(db_fetch_array($result)));
      }
      return $additions;

    case 'insert':
      foreach ($node_field as $delta => $item) {
        $data = array();
        $column_names = array();
        $column_placeholders = array();
        $column_assignments = array();
        foreach ($db_info['columns'] as $column => $attributes) {
          $column_names[] = $attributes['column'];
          switch ($attributes['type']) {
            case 'int':
            case 'mediumint':
            case 'tinyint':
            case 'bigint':
              $column_placeholders[] = '%d';
              $column_assignments[] = $attributes['column'] .' = %d';
              break;
            case 'float':
              $column_placeholders[] = '%f';
              $column_assignments[] = $attributes['column'] .' = %f';
              break;
            default:
              $column_placeholders[] = "'%s'";
              $column_assignments[] = $attributes['column'] ." = '%s'";
          }
          $data[] = $item[$column];
        }
        $data[] = $node->vid;
        $data[] = $node->nid;
        if ($field['multiple']) {
          $data[] = $delta;
        }

        if ($field['multiple']) {
          db_query('INSERT INTO {'. $db_info['table'] .'} ('. implode(', ', $column_names) .', vid, nid, delta) VALUES ('. implode(', ', $column_placeholders) .', %d, %d, %d)', $data);
        }
        else {
          if (db_result(db_query('SELECT COUNT(*) FROM {'. $db_info['table'] .'} WHERE vid = %d AND nid = %d', $node->vid, $node->nid))) {
            db_query('UPDATE {'. $db_info['table'] .'} SET '. implode(', ', $column_assignments) .' WHERE vid = %d AND nid = %d', $data);
          }
          else {
            db_query('INSERT INTO {'. $db_info['table'] .'} ('. implode(', ', $column_names) .', vid, nid) VALUES ('. implode(', ', $column_placeholders) .', %d, %d)', $data);
          }
        }
      }
      return;

    case 'update':
      if ($field['multiple']) {
        // Delete and insert, rather than update, in case a field was added.
        db_query('DELETE FROM {'. $db_info['table'] .'} WHERE vid = %d', $node->vid);
      }

      foreach ($node_field as $delta => $item) {
        $data = array();
        $column_names = array();
        $column_placeholders = array();
        $column_assignments = array();
        foreach ($db_info['columns'] as $column => $attributes) {
          $column_names[] = $attributes['column'];
          switch ($attributes['type']) {
            case 'int':
            case 'mediumint':
            case 'tinyint':
            case 'bigint':
              $column_placeholders[] = '%d';
              $column_assignments[] = $attributes['column'] .' = %d';
              break;
            case 'float':
              $column_placeholders[] = '%f';
              $column_assignments[] = $attributes['column'] .' = %f';
              break;
            default:
              $column_placeholders[] = "'%s'";
              $column_assignments[] = $attributes['column'] ." = '%s'";
          }
          $data[] = $item[$column];
        }
        $data[] = $node->vid;
        $data[] = $node->nid;
        if ($field['multiple']) {
          $data[] = $delta;
        }

        if ($field['multiple']) {
          db_query('INSERT INTO {'. $db_info['table'] .'} ('. implode(', ', $column_names) .', vid, nid, delta) VALUES ('. implode(', ', $column_placeholders) .', %d, %d, %d)', $data);
        }
        else {
          if (db_result(db_query('SELECT COUNT(*) FROM {'. $db_info['table'] .'} WHERE vid = %d AND nid = %d', $node->vid, $node->nid))) {
            db_query('UPDATE {'. $db_info['table'] .'} SET '. implode(', ', $column_assignments) .' WHERE vid = %d AND nid = %d', $data);
          }
          else {
            db_query('INSERT INTO {'. $db_info['table'] .'} ('. implode(', ', $column_names) .', vid, nid) VALUES ('. implode(', ', $column_placeholders) .', %d, %d)', $data);
          }
        }
      }
      return;

    case 'delete':
      // Delete using nid rather than vid to purge all revisions.
      db_query('DELETE FROM {'. $db_info['table'] .'} WHERE nid = %d', $node->nid);
      return;

    case 'delete revision':
      db_query('DELETE FROM {'. $db_info['table'] .'} WHERE vid = %d', $node->vid);
      return;
  }
}


/**
 * Invoke a field hook.
 *
 * For each operation, both this function and _content_field_invoke_default() are
 * called so that the default database handling can occur.
 */
function _content_field_invoke($op, &$node, $teaser = NULL, $page = NULL) {
  $type_name = is_string($node) ? $node : (is_array($node) ? $node['type'] : $node->type);
  $type = content_types($type_name);
  $field_types = _content_field_types();

  $return = array();
  foreach ($type['fields'] as $field) {
    $node_field = isset($node->$field['field_name']) ? $node->$field['field_name'] : array();

    $module = $field_types[$field['type']]['module'];
    $function = $module .'_field';
    if (function_exists($function)) {
      $result = $function($op, $node, $field, $node_field, $teaser, $page);
      if (is_array($result)) {
        $return = array_merge($return, $result);
      }
      else if (isset($result)) {
        $return[] = $result;
      }
    }

    if (isset($node->$field['field_name'])) {
      $node->$field['field_name'] = $node_field;
    }
  }

  return $return;
}

/**
 * Invoke content.module's version of a field hook.
 */
function _content_field_invoke_default($op, &$node, $teaser = NULL, $page = NULL) {
  $type_name = is_string($node) ? $node : (is_array($node) ? $node['type'] : $node->type);
  $type = content_types($type_name);
  $field_types = _content_field_types();

  $return = array();
  foreach ($type['fields'] as $field) {
    $node_field = isset($node->$field['field_name']) ? $node->$field['field_name'] : array();

    $db_info = content_database_info($field);
    if (count($db_info['columns'])) {
      $result = content_field($op, $node, $field, $node_field, $teaser, $page);
      if (is_array($result)) {
        $return = array_merge($return, $result);
      }
      else if (isset($result)) {
        $return[] = $result;
      }
    }

    if (isset($node->$field['field_name'])) {
      $node->$field['field_name'] = $node_field;
    }
  }

  return $return;
}

/**
 * Invoke a widget hook.
 */
function _content_widget_invoke($op, &$node) {
  $type_name = is_string($node) ? $node : (is_array($node) ? $node['type'] : $node->type);
  $type = content_types($type_name);
  $widget_types = _content_widget_types();

  $return = array();
  foreach ($type['fields'] as $field) {
    $node_field = isset($node->$field['field_name']) ? $node->$field['field_name'] : array();

    $module = $widget_types[$field['widget']['type']]['module'];
    $function = $module .'_widget';
    if (function_exists($function)) {
      $result = $function($op, $node, $field, $node_field);
      if (is_array($result)) {
        $return = array_merge($return, $result);
      }
      else if (isset($result)) {
        $return[] = $result;
      }
    }

    if (is_object($node) && isset($node->$field['field_name'])) {
      $node->$field['field_name'] = $node_field;
    }
  }

  return $return;
}

/**
 * Return a list of all content types.
 *
 * @param $content_type_name
 *   If set, return information on just this type.
 */
function content_types($content_type_name = NULL) {
  $info = _content_type_info();
  if (isset($content_type_name)) {
    if (isset($info['content types'][$content_type_name])) {
      return $info['content types'][$content_type_name];
    }
    else {
      return NULL;
    }
  }
  return $info['content types'];
}

/**
 * Return a list of all fields.
 *
 * @param $field_name
 *   If set, return information on just this field.
 * @param $content_type_name
 *   If set, return information of the field within the context of this content
 *   type.
 */
function content_fields($field_name = NULL, $content_type_name = NULL) {
  $info = _content_type_info();
  if (isset($field_name)) {
    if (isset($info['fields'][$field_name])) {
      if (isset($content_type_name)) {
        if (isset($info['content types'][$content_type_name]['fields'][$field_name])) {
          return $info['content types'][$content_type_name]['fields'][$field_name];
        }
        else {
          return NULL;
        }
      }
      else {
        return $info['fields'][$field_name];
      }
    }
    else {
      return NULL;
    }
  }
  return $info['fields'];
}

/**
 * Return a list of field types.
 */
function _content_field_types() {
  $info = _content_type_info();
  return $info['field types'];
}

/**
 * Return a list of widget types.
 */
function _content_widget_types() {
  $info = _content_type_info();
  return $info['widget types'];
}

/**
 * Collate all information on content types, fields, and related structures.
 *
 * @param $reset
 *   If TRUE, clear the cache and fetch the information from the database again.
 */
function _content_type_info($reset = FALSE) {
  static $info;

  if ($reset || !isset($info)) {
    if ($cached = cache_get('content_type_info')) {
      $info = unserialize($cached->data);
    }
    else {
      $info = array(
        'field types' => array(),
        'widget types' => array(),
        'fields' => array(),
        'content types' => array(),
      );

      foreach (module_list() as $module) {
        $module_field_types = module_invoke($module, 'field_info');
        if ($module_field_types) {
          foreach ($module_field_types as $name => $field_info) {
            $info['field types'][$name] = $field_info;
            $info['field types'][$name]['module'] = $module;
            $info['field types'][$name]['formatters'] = array();
          }
        }

        $module_widgets = module_invoke($module, 'widget_info');
        if ($module_widgets) {
          foreach ($module_widgets as $name => $widget_info) {
            $info['widget types'][$name] = $widget_info;
            $info['widget types'][$name]['module'] = $module;
          }
        }
      }

      foreach (module_list() as $module) {
        $module_formatters = module_invoke($module, 'field_formatter_info');
        if ($module_formatters) {
          foreach ($module_formatters as $name => $formatter_info) {
            foreach ($formatter_info['field types'] as $field_type) {
              $info['field types'][$field_type]['formatters'][$name] = $formatter_info;
              $info['field types'][$field_type]['formatters'][$name]['module'] = $module;
            }
          }
        }
      }

      $field_result = db_query('SELECT * FROM {node_field} nf');
      while ($field = db_fetch_array($field_result)) {
        $global_settings = $field['global_settings'] ? unserialize($field['global_settings']) : array();
        unset($field['global_settings']);
        $field = array_merge($field, $global_settings);
        $instance_info = db_fetch_array(db_query("SELECT type_name, label FROM {node_field_instance} WHERE field_name = '%s'", $field['field_name']));
        $field['widget']['label'] = $instance_info['label'];
        $field['type_name'] = $instance_info['type_name'];
        $info['fields'][$field['field_name']] = $field;
      }

      $type_result = db_query('SELECT * FROM {node_type_content} nt ORDER BY nt.type_name ASC');
      while ($type = db_fetch_array($type_result)) {
        $type['fields'] = array();
        $field_result = db_query("SELECT nfi.field_name, nfi.weight, nfi.label, nfi.widget_type, nfi.widget_settings, nfi.description FROM {node_field_instance} nfi WHERE nfi.type_name = '%s' ORDER BY nfi.weight ASC, nfi.label ASC", $type['type_name']);
        while ($field = db_fetch_array($field_result)) {
          // Overwrite global field information with specific information
          $field = array_merge($info['fields'][$field['field_name']], $field);

          $widget_settings = $field['widget_settings'] ? unserialize($field['widget_settings']) : array();
          unset($field['widget_settings']);
          $field['widget'] = $widget_settings;
          $field['widget']['type'] = $field['widget_type'];
          unset($field['widget_type']);
          $field['widget']['weight'] = $field['weight'];
          unset($field['weight']);
          $field['widget']['label'] = $field['label'];
          unset($field['label']);
          $field['widget']['description'] = $field['description'];
          unset($field['description']);
          $field['type_name'] = $type['type_name'];
          $type['fields'][$field['field_name']] = $field;
        }

        $info['content types'][$type['type_name']] = $type;
      }

      cache_set('content_type_info', serialize($info), CACHE_PERMANENT);
    }
  }

  return $info;
}

/**
 * Clear the cache of content_types; called in several places when content
 * information is changed.
 */
function content_clear_type_cache() {
  cache_clear_all('content_type_info');

  _content_type_info(TRUE);

  if (module_exist('views')) {
    views_invalidate_cache();
  }
}

/**
 * Retrieve the database storage location(s) for a field.
 *
 * @param $field
 *   The field whose database information is requested.
 * @return
 *   An array with the keys:
 *     "table": The name of the database table where the field data is stored.
 *     "columns": An array of columns stored for this field. Each is a collection
 *       of information returned from hook_field_settings('database columns'),
 *       with the addition of a "column" attribute which holds the name of the
 *       database column that stores the data.
 */
function content_database_info($field) {
  $field_types = _content_field_types();
  $module = $field_types[$field['type']]['module'];
  $columns = module_invoke($module, 'field_settings', 'database columns', $field);

  $db_info = array();

  if ($field['db_storage'] == CONTENT_DB_STORAGE_PER_FIELD) {
    $db_info['table'] = 'node_data_'. $field['field_name'];
  }
  else {
    $db_info['table'] = 'node_'. $field['type_name'];
  }

  if (is_array($columns) && count($columns)) {
    $db_info['columns'] = $columns;
    foreach ($columns as $column_name => $attributes) {
      $db_info['columns'][$column_name]['column'] = $field['field_name'] .'_'. $column_name;
    }
  }
  else {
    $db_info['columns'] = array();
  }

  return $db_info;
}

/**
 * Manipulate a 2D array to reverse rows and columns.
 *
 * The default data storage for fields is delta first, column names second.
 * This is sometimes inconvenient for field modules, so this function can be
 * used to present the data in an alternate format.
 *
 * @param $array
 *   The array to be transposed. It must be at least two-dimensional, and
 *   the subarrays must all have the same keys or behavior is undefined.
 *
 * @return
 *   The transposed array.
 */
function content_transpose_array_rows_cols($array) {
  $result = array();
  if (is_array($array)) {
    foreach ($array as $key1 => $value1) {
      if (is_array($value1)) {
        foreach ($value1 as $key2 => $value2) {
          if (!isset($result[$key2])) {
            $result[$key2] = array();
          }
          $result[$key2][$key1] = $value2;
        }
      }
    }
  }
  return $result;
}

/**
 * Format a field item for display.
 *
 * @param $field
 *   Either a field array or the name of the field.
 * @param $item
 *   The field item to be formatted (such as $node->field_foo[0]).
 * @param $formatter
 *   The name of the formatter to use.
 * @param $node
 *   Optionally, the containing node object for context purposes.
 *
 * @return
 *   A string containing the contents of the field item sanitized for display.
 *   It will have been passed through the necessary check_plain() or check_markup()
 *   functions as necessary.
 */
function content_format($field, $item, $formatter = 'default', $node = NULL) {
  if (!is_array($field)) {
    $field = content_fields($field);
  }
  $field_types = _content_field_types();
  $formatters = $field_types[$field['type']]['formatters'];

  if (!isset($formatters[$formatter])) {
    $formatter = 'default';
  }
  return module_invoke($formatters[$formatter]['module'], 'field_formatter', $field, $item, $formatter, $node);
}

/**
 * Format an individual field for display.
 *
 * @param $node
 *   The node being displayed (provided for context).
 * @param $field
 *   The field that is to be displayed (for information about the label, field
 *   type, and so forth).
 * @param $items
 *   The actual items to theme. This will be a linear array, each element of
 *   which has a "view" property which contains the filtered, formatted contents
 *   of the item.
 * @param $teaser
 *   Whether the node is being displayed as a teaser or full version.
 * @param $page
 *   Whether the node is being displayed as a full web page.
 *
 * @return
 *   An HTML string containing the fully themed field.
 */
function theme_field(&$node, &$field, &$items, $teaser, $page) {
  $output = '';

  $output .= '<div class="field field-type-'. strtr($field['type'], '_', '-') .' field-'. strtr($field['field_name'], '_', '-') .'">';

  $output .= '<h3 class="field-label">'. $field['widget']['label'] .'</h3>';

  $output .= '<div class="field-items">';
  foreach ($items as $item) {
    $output .= '<div class="field-item">'. $item['view'] .'</div>';
  }
  $output .= '</div>';

  $output .= '</div>';

  return $output;
}
