<?php
// $Id: sitedoc.module,v 1.45 2008/02/05 00:44:05 nancyw Exp $

/**
 * Implementation of hook_help().
 */
function sitedoc_help($path, $arg=array()) {
  switch ($path) {
    case 'admin/help#sitedoc':
      $output = '<p>'. t('The sitedoc module allows a site admin to gather extensive information about their site. It may be printed or archived for configuration/change management purposes. It may also be used to add "fancy" and up-to-date information to other site documentation, such as a succession plan.') .'</p>';
      break;
      
    case 'admin/settings/sitedoc/archive':
      $output = '<p>'. t('The Site Documentation module may be run via Cron and the output HTML file archived to disk for future review. The options chosen on the main settings page will govern the data collected and reported.') .'</p>';
      break;
      
    case 'admin/settings/sitedoc':
      $output = '<p>'. t("The Site Documentation module's data collection and output is governed by these settings. Indented options are in effect only if the parent item is selected.") .'</p>';
  }
  return $output;
}

/**
 * Implementation of hook_perm().
 */
function sitedoc_perm() {
  return array('view site documentation');
}

/**
 * Implementation of hook_menu().
 *
 * Add a menu item to the Administer >> Site building menu for displaying the sitedoc.
 * Add a menu item to the Administer >> Site configuration menu for the sitedoc settings.
 * Add a menu callback item to the Administer >> Site building menu for displaying node access lists.
 * Add a menu callback item to the menu for displaying taxonomy term counts.
 * Add a menu callback item to the menu for displaying phpinfo.
 */
function sitedoc_menu($may_cache) {
 $items = array();

  if ($may_cache) {
    drupal_add_css(drupal_get_path('module', 'sitedoc') .'/sitedoc.css');
    $items[] = array(
      'path' => 'admin/build/sitedoc',
      'title' => t('Site documentation'),
      'description' => t('Show site documentation'),
      'callback' => 'sitedoc_list',
      'access' => user_access('view site documentation'),
      );
    $items[] = array(
      'path' => 'admin/settings/sitedoc',
      'title' => t('Site documentation'),
      'description' => t('Set how Site Documentation works'),
      'callback' => 'sitedoc_settings_page',
      'access' => user_access('view site documentation'),
      );  
    $items[] = array(
      'path' => 'admin/settings/sitedoc/report',
      'title' => t('Report'),
      'description' => t('Report settings'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array('sitedoc_settings_form'),
      'access' => user_access('view site documentation'),
      'type' => MENU_DEFAULT_LOCAL_TASK,
      );  
    $items[] = array(
      'path' => 'admin/settings/sitedoc/archive',
      'title' => t('Archive'),
      'access' => user_access('view site documentation'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array('sitedoc_archive_form'),
      'description' => t('Set how Site Documentation archive works'),
      'weight' => 2,
      'type' => MENU_LOCAL_TASK,
    );
   }
  else {
    $items[] = array(
      'path' => 'admin/build/sitedoc_node_access_view',
      'title' => t('Site documentation node access list'),
      'description' => t('Do not use'),
      'callback' => 'sitedoc_node_access_view',
      'callback arguments' => array(arg(3)),
      'access' => user_access('view site documentation'),
      'type' => MENU_CALLBACK,
      );
    $items[] = array('path' => 'sitedoc/vocabulary',
      'title' => t('Site Documentation Term Count by Type'),
      'callback' => 'sitedoc_term_count_by_type',
//      'callback arguments' => array(arg(2)),
      'access' => user_access('access content'),
      'type' => MENU_CALLBACK
      );
    $items[] = array('path' => 'sitedoc/phpinfo',
      'title' => t('Site Documentation PHP Information'),
      'callback' => 'sitedoc_phpinfo',
      'access' => user_access('view site documentation'),
      'type' => MENU_CALLBACK
      );
    $items[] = array('path' => 'sitedoc/table',
      'title' => t('Table Contents'),
      'callback' => 'sitedoc_show_table',
      'access' => user_access('access content'),
      'type' => MENU_CALLBACK
      );
   }
  return $items; 
}

/**
 * Implementation of hook_enable().
 *
 * Set the default parameters. This is an easy way to reset the deaults - disable and re-enable.
 *
 *  Parameters: None.
 *
 *  Return: None.
 */
function sitedoc_enable() {
  $sitedoc_settings = array(
                         'drupal_section' => TRUE,
                           'kill_cron' => 0,                /* kill long-running cron */
                         'table_section' => TRUE,
                           'show_indexes' => FALSE,         /* use SHOW INDEX on the tables */
                           'optimize_tables' => FALSE,      /* use OPTIMIZE to release overhead */
                         'sequences_section' => FALSE,      /* not on because not much interest */
                         'node_section' => FALSE,           /* not on because of expense */
                           'include_comment_count' => FALSE,  /* comment count on nodes */
                           'include_node_access' => FALSE,  /* node access summary */
                           'node_show_size' => 99999,       /* show nodes exceeding x KB */
                           'node_max_size' => 50,           /* warn if nodes exceeding x KB */
                         'module_section' => TRUE,
                           'module_suppress' => FALSE,      /* exclude disabled modules */
                           'module_sort_order' => 0,               /* package, project, module */
                         'content_section' => TRUE,
                         'vocabulary_section' => TRUE,
                           'orphan_term_node' => FALSE,     /* not on to allow control */
                           'delete_orphan_term_nodes' => FALSE,  /* do not delete orphans  */
                         'theme_section' => TRUE,
                         'variables_section' => FALSE,      /* not on because of expense */
                         'block_section' => TRUE,
                           'block_warn' => TRUE,            /* warn on missing theme */
                           'block_delete' => FALSE,         /* delete those orphan blocks */
                         'roles_section' => TRUE,
                           'role_users' => TRUE,            /* list users in roles */
                           'role_perms_list' => FALSE,      /* do role permissions as stream */
                         'contacts_section' => TRUE,
                         'profile_fields_section' => FALSE, /* not on because of interest */
                         'url_alias_section' => FALSE,      /* not on because of expense */
                         'input_format_section' => FALSE,   /* not on because probably not wanted */
    //                Archive options
                         'archive_frequency' => 0,          /* don't run */
                         'archive_directory' => 'sitedoc',  /* archive directory within file setting */ 
                     );
  variable_set('sitedoc_settings', $sitedoc_settings);
}

/********************************/
/**   Main settings function   **/
/********************************/
function sitedoc_settings_page() {
  return drupal_get_form('sitedoc_settings_form');
}  

function sitedoc_settings_form() {
  drupal_add_css(drupal_get_path('module', 'sitedoc') .'/sitedoc.css');
  $sitedoc_settings = variable_get('sitedoc_settings', array());

// Sections Fieldset 
  $form['sections'] = array(
    '#type' => 'fieldset', 
    '#title' => t('Show Sections'), 
    '#weight' => -5,
    '#collapsible' => FALSE,
    '#collapsed' => FALSE,
    '#prefix' => '<div class="sitedoc_sections_block">',
    '#suffix' => '',
  );

  $form['sections']['drupal_section'] = array(
    '#type' => 'checkbox',
    '#title' => t('Include Basic Drupal information?'),
    '#prefix' => '<div class="sitedoc_sections">',
//    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['drupal_section'],
  );

  $kill_time = drupal_map_assoc(array(0, 1800, 3600, 7200, 10800, 14400, 18000, 21600, 32400, 43200, 86400, 172800), 'format_interval');
  $kill_time['0'] = t('Never');
  $form['sections']['kill_cron'] = array(
    '#type' => 'select',
    '#options' => $kill_time,
    '#title' => t('Delete Cron variables after'),
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div></div>',
    '#default_value' => $sitedoc_settings['kill_cron'],
  ); 

  $form['sections']['table_section'] = array(
    '#type' => 'checkbox',
    '#title' => t('Include Table Summary?'),
    '#prefix' => '<div class="sitedoc_sections">',
//    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['table_section'],
  );

  $form['sections']['show_indexes'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show indexes?'),
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['show_indexes'],
  );

  $form['sections']['optimize_tables'] = array(
    '#type' => 'checkbox',
    '#title' => t('Release overhead?'),
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div></div>',
    '#description' => t('(SQL intensive - uses OPTIMIZE)'),
    '#default_value' => $sitedoc_settings['optimize_tables'],
  );

// Postgres databases don't use the sequences table (and D6 probably won't either)
  if ($GLOBALS['db_type'] == 'postgres') {
    // Force it off and skip the form field
    $sitedoc_settings['sequences_section'] = FALSE;
  }
  else {  
    $form['sections']['sequences_section'] = array(
      '#type' => 'checkbox',
      '#title' => t('Include Sequences Summary?'),
      '#prefix' => '<div class="sitedoc_sections">',
      '#suffix' => '</div>',
      '#default_value' => $sitedoc_settings['sequences_section'],
   );
  }

  $form['sections']['node_section'] = array(
    '#type' => 'checkbox',
    '#title' => t('Include Node Summary?'),
    '#prefix' => '<div class="sitedoc_sections">',
//    '#suffix' => '</div>',
    '#description' => t('(SQL intensive)'),
    '#default_value' => $sitedoc_settings['node_section'],
  );

  $form['sections']['node_show_size'] = array(
    '#type' => 'textfield',
    '#title' => t('Show nodes exceeding (KB)'),
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['node_show_size'],
    '#size' => 8,
    '#maxlength' => 7,
  );

  $form['sections']['node_max_size'] = array(
    '#type' => 'textfield',
    '#title' => t('Warn if nodes exceed (KB)'),
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['node_max_size'],
    '#size' => 8,
    '#maxlength' => 7,
  );

  $form['sections']['include_node_access'] = array(
    '#type' => 'checkbox',
    '#title' => t('Include node access summary?'),
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['include_node_access'],
  );

  $form['sections']['include_comment_count'] = array(
    '#type' => 'checkbox',
    '#title' => t('Include comment count on nodes?'),
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div></div>',
    '#default_value' => $sitedoc_settings['include_comment_count'],
  );

  $form['sections']['variables_section'] = array(
    '#type' => 'checkbox',
    '#title' => t('Include System Variables?'),
    '#prefix' => '<div class="sitedoc_sections">',
    '#suffix' => '</div>',
    '#description' => t('(CPU intensive)'),
    '#default_value' => $sitedoc_settings['variables_section'],
  );

  $form['sections']['module_section'] = array(
    '#type' => 'checkbox',
    '#title' => t('Include Module summary?'),
    '#prefix' => '<div class="sitedoc_sections">',
//    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['module_section'],
  );

  $form['sections']['module_suppress'] = array(
    '#type' => 'checkbox',
    '#title' => t('Exclude disabled modules?'),
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['module_suppress'],
  );

  $module_sort_order = array(
    0 => 'package, project, module',
    1 => 'path, module',
    );

  $form['sections']['module_sort_order'] = array(
    '#type' => 'radios',
    '#title' => t('List sort order'),
    '#options' => $module_sort_order,
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div></div>',
    '#default_value' => $sitedoc_settings['module_sort_order'],
  );

  $form['sections']['content_section'] = array(
    '#type' => 'checkbox',
    '#title' => t('Include Content Type summary?'),
    '#prefix' => '<div class="sitedoc_sections">',
    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['content_section'],
  );

  if (module_exists('taxonomy')) {
    $form['sections']['vocabulary_section'] = array(
      '#type' => 'checkbox',
      '#title' => t('Include Vocabulary summary?'),
      '#prefix' => '<div class="sitedoc_sections">',
//      '#suffix' => '</div>',
      '#description' => t('(may be CPU and SQL intensive)'),
      '#default_value' => $sitedoc_settings['vocabulary_section'],
    );

  $form['sections']['orphan_term_node'] = array(
    '#type' => 'checkbox',
    '#title' => t('Check for orphan Term_nodes?'),
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div>',
    '#description' => t('(may be SQL intensive)'),
    '#default_value' => $sitedoc_settings['orphan_term_node'],
  );

  $form['sections']['delete_orphan_term_nodes'] = array(
    '#type' => 'checkbox',
    '#title' => t('Delete orphan term nodes?'),
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div></div>',
    '#default_value' => $sitedoc_settings['delete_orphan_term_nodes'],
  );
   }  /* end if taxo exists */

  $form['sections']['theme_section'] = array(
    '#type' => 'checkbox',
    '#title' => t('Include Themes and Theme Engines?'),
    '#prefix' => '<div class="sitedoc_sections">',
    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['theme_section'],
  );

  $form['sections']['block_section'] = array(
    '#type' => 'checkbox',
    '#title' => t('Include Blocks and Boxes?'),
    '#prefix' => '<div class="sitedoc_sections">',
//    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['block_section'],
  );

  $form['sections']['block_warn'] = array(
    '#type' => 'checkbox',
    '#title' => t('Warn on missing theme for blocks?'),
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div>',
    '#description' => t('This allows the module to display a warning message if a block exists for a missing theme.'),
    '#default_value' => $sitedoc_settings['block_warn'],
  );

  $form['sections']['block_delete'] = array(
    '#type' => 'checkbox',
    '#title' => t('Delete the orphan blocks?'),
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div></div>',
    '#description' => t('Delete the blocks with a missing theme.'),
    '#default_value' => $sitedoc_settings['block_delete'],
  );

  $form['sections']['roles_section'] = array(
    '#type' => 'checkbox',
    '#title' => t('Include Roles and Permissions?'),
    '#prefix' => '<div class="sitedoc_sections">',
//    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['roles_section'],
  );

  $form['sections']['role_users'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show list of users for each role?'),
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div>',
    '#description' => t('Displays the list of users assigned the role.'),
    '#default_value' => $sitedoc_settings['role_users'],
  );

  $form['sections']['role_perms_list'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show role permissions as a list?'),
    '#prefix' => '<div class="sitedoc_options">',
    '#suffix' => '</div></div>',
    '#description' => t('If not chosen, the roles will be shown as a stream.'),
    '#default_value' => $sitedoc_settings['role_perms_list'],
  );

  if (module_exists('contact')) {
    $form['sections']['contacts_section'] = array(
      '#type' => 'checkbox',
      '#title' => t('Include Contacts?'),
      '#prefix' => '<div class="sitedoc_sections">',
      '#suffix' => '</div>',
      '#default_value' => $sitedoc_settings['contacts_section'],
    );
   }

  if (module_exists('profile')) {
    $form['sections']['profile_fields_section'] = array(
      '#type' => 'checkbox',
      '#title' => t('Include Profile Fields?'),
      '#prefix' => '<div class="sitedoc_sections">',
      '#suffix' => '</div>',
      '#default_value' => $sitedoc_settings['profile_fields_section'],
    );
   }

  if (module_exists('path')) {
    $form['sections']['url_alias_section'] = array(
      '#type' => 'checkbox',
      '#title' => t('Include URL Aliases?'),
      '#prefix' => '<div class="sitedoc_sections">',
      '#suffix' => '</div>',
      '#description' => t('(SQL intensive)'),
      '#default_value' => $sitedoc_settings['url_alias_section'],
    );
   }

  $form['sections']['input_format_section'] = array(
    '#type' => 'checkbox',
    '#title' => t('Include Input Formats and Filters?'),
    '#prefix' => '<div class="sitedoc_sections">',
    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['input_format_section'],
    );

// Update Button
  $form['save']['attach'] = array(
    '#type' => 'submit',
    '#value' => t('Save configuration'),
    '#weight' => 5,
    '#prefix' => '</div><div class="clear-block"></div>'
  );

  return $form;
}

/**
 * Save the settings values.
 */
function sitedoc_settings_form_submit($form_id, $form_values) {
// update the settings array and save it
  $settings = variable_get('sitedoc_settings', array());

  $settings['drupal_section'] = $form_values['drupal_section'];
  $settings['kill_cron'] = $form_values['kill_cron'];
  $settings['table_section'] = $form_values['table_section'];
  $settings['show_indexes'] = $form_values['show_indexes'];
  $settings['optimize_tables'] = $form_values['optimize_tables'];
  $settings['sequences_section'] = $form_values['sequences_section'];
  $settings['node_section'] = $form_values['node_section'];
  $settings['include_comment_count'] = $form_values['include_comment_count'];
  $settings['include_node_access'] = $form_values['include_node_access'];
  $settings['node_show_size'] = $form_values['node_show_size'];
  $settings['node_max_size'] = $form_values['node_max_size'];
  $settings['module_section'] = $form_values['module_section'];
  $settings['module_suppress'] = $form_values['module_suppress'];
  $settings['module_sort_order'] = $form_values['module_sort_order'];
  $settings['content_section'] = $form_values['content_section'];
  $settings['vocabulary_section'] = $form_values['vocabulary_section'];
  $settings['orphan_term_node'] = $form_values['orphan_term_node'];
  $settings['delete_orphan_term_nodes'] = $form_values['delete_orphan_term_nodes'];
  $settings['theme_section'] = $form_values['theme_section'];
  $settings['variables_section'] = $form_values['variables_section'];
  $settings['block_section'] = $form_values['block_section'];
  $settings['block_warn'] = $form_values['block_warn'];
  $settings['block_delete'] = $form_values['block_delete'];
  $settings['roles_section'] = $form_values['roles_section'];
  $settings['role_users'] = $form_values['role_users'];
  $settings['role_perms_list'] = $form_values['role_perms_list'];
  $settings['contacts_section'] = $form_values['contacts_section'];
  $settings['profile_fields_section'] = $form_values['profile_fields_section'];
  $settings['url_alias_section'] = $form_values['url_alias_section'];
  $settings['input_format_section'] = $form_values['input_format_section'];

  variable_set('sitedoc_settings', $settings);

  drupal_set_message(t('Configuration has been updated. ') . l(t('Run now'), 'admin/build/sitedoc'), 'notice'); 
}

function sitedoc_archive_form($form_values=NULL) {
  drupal_add_css(drupal_get_path('module', 'sitedoc') .'/sitedoc.css');
  $sitedoc_settings = variable_get('sitedoc_settings', array());

  $save_time = drupal_map_assoc(array(0, 3600, 7200, 14400, 21600, 43200, 86400, 172800, 259200, 604800), 'format_interval');
  $save_time['0'] = t('Never');
  $save_time['999999'] = t('Always');

  $form['archive_frequency'] = array(
    '#type' => 'select',
    '#options' => $save_time,
    '#title' => t('Archive frequency'),
    '#prefix' => '<div class="sitedoc_archive">',
    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['archive_frequency'],
    '#description' => t('The Site Documentation module will run at the next scheduled Cron run after this period has elapsed. A zero value suppresses creation of an archive. "Always" means a file will be created every time Cron runs.'),
  );

  $form['archive_directory'] = array(
    '#type' => 'textfield',
    '#title' => t('Archive directory'),
    '#prefix' => '<div class="sitedoc_archive">',
    '#suffix' => '</div>',
    '#default_value' => $sitedoc_settings['archive_directory'],
    '#description' => t('This is directory within \'!filepath\' where the Site Documentation module will place the archive file. The current date will be appended to the file name.', array('!filepath' => variable_get('file_directory_path', 'files'))),
  );

// Add the Buttons
  $form['save'] = array(
    '#type' => 'submit',
    '#value' => t('Save configuration'),
    '#weight' => 5,
    '#prefix' => '</div><div class="clear-block"></div>'
  );

  // If $form_values exists, then we've been here before (submitted), so now add a "run cron" button.
  if (isset($form_values)) {
    $form['cron'] = array(
      '#type' => 'submit',
      '#value' => t('Run Cron Now'),
      '#weight' => 6,
    );
  }

  // These allow us to add on the Cron button.
  $form['#multistep'] = TRUE;
  $form['#redirect'] = FALSE;

  return $form;
}

/**
 * Validate the submission.
 */
function sitedoc_archive_form_validate($form_id, $form_values) {
  $archive_path = variable_get('file_directory_path', 'files') .'/'. $form_values['archive_directory'];
  if (!file_check_directory($archive_path, TRUE, 'archive_directory')) {
    form_set_error('archive_directory', t('The archive path could not be found or could not be created.'));
   }  // end path not found 
}

/**
 * Save the settings values.
 */
function sitedoc_archive_form_submit($form_id, $form_values) {
  switch ($form_values['op']) {
    case t('Run Cron Now'):
      drupal_goto('admin/logs/status/run-cron');
      break;

    case t('Save configuration'):
      // update the settings array and save it
      $sitedoc_settings = variable_get('sitedoc_settings', array());
      $sitedoc_settings['archive_frequency'] = $form_values['archive_frequency'];
      $sitedoc_settings['archive_directory'] = $form_values['archive_directory'];

      variable_set('sitedoc_settings', $sitedoc_settings);

      drupal_set_message(t('Configuration has been updated. ') . l(t('Run now'), 'admin/build/sitedoc'), 'notice');
  }
}

/***********************/
/**   main function   **/
/***********************/
function sitedoc_list() {
  // Retrieve the settings and check if they have been settings page.
  $sitedoc_settings = variable_get('sitedoc_settings', array());
  if ($sitedoc_settings == array()) {
    return t('It appears that you have not been to "administer >> site configuration" to set the Site Documentation settings. ') . l(t('Click here to do so.'), 'admin/settings/sitedoc');
   }

  // Insert a wrapper division and a link back to the settings page. 
  $output = '<div class="site-documentation">'. l(t('Change settings.'), 'admin/settings/sitedoc') .'<br/><br/>';

  // Get basic Drupal info.
  if ($sitedoc_settings['drupal_section']) {
    $output .= '<div class="sitedoc_drupal">';
    $fieldset = array(
      '#title' => t('Drupal Section'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => sitedoc_drupal($sitedoc_settings['kill_cron']),
    );  
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";
   }

  // Get database info.
  if ($sitedoc_settings['table_section']) {
    $output .= '<div class="sitedoc_database_overview">';
    $fieldset = array(
      '#title' => t('Table Section'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => sitedoc_database_overview($sitedoc_settings['optimize_tables'], $sitedoc_settings['show_indexes']),
    );  
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";
   }
 
   // Get info from the sequences table (last used item number).
  if ($sitedoc_settings['sequences_section']) {
    $output .= '<div class="sitedoc_sequences">';
    $fieldset = array(
      '#title' => t('Sequences Section'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => sitedoc_sequences(),
    );  
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";
   }
 
  // Get summary of all nodes (this is VERY expensive!).
  if ($sitedoc_settings['node_section']) {
    $output .= '<div class="sitedoc_node_summary">';
    $fieldset = array(
      '#title' => t('Node Summary'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => sitedoc_node_summary($sitedoc_settings['include_comment_count'], $sitedoc_settings['node_show_size'], $sitedoc_settings['node_max_size']),
      );
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";
 
    // Get node access summary (this is expensive!).
    // Runs only if the node summary section is selected.
    if ($sitedoc_settings['include_node_access']) {
      $output .= '<div class="sitedoc_node_access">';
      $fieldset = array(
        '#title' => t('Node Access'),
        '#collapsible' => TRUE,
        '#collapsed' => FALSE,
        '#value' => sitedoc_node_access(),
      );  
      $output .=  theme('fieldset', $fieldset);
      $output .= "</div>\n";
     } // end access summary 
   }  // end node summary 

  // Get content type info from node module along with workflow options from system variables.
  if ($sitedoc_settings['content_section']) {
    $output .= '<div class="sitedoc_content_types">';
    $fieldset = array(
      '#title' => t('Content Types'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => sitedoc_content_types(),
    );  
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";
   }

  // Get modules from the System module functions.
  if ($sitedoc_settings['module_section']) {
    $output .= '<div class="sitedoc_modules">';
    $fieldset = array(
      '#title' => t('Modules'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => sitedoc_get_modules($sitedoc_settings['module_suppress'], $sitedoc_settings['module_sort_order']),
    );  
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";
   }

  // Get themes and theme_engines from the System table.
  if ($sitedoc_settings['theme_section']) {  /* controls themes and theme engines */
    $output .= '<div class="sitedoc_themes">';
    $fieldset = array(
      '#title' => t('Themes'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => sitedoc_get_system('theme'),
    );  
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";

    $output .= '<div class="sitedoc_theme_engines">';
    $fieldset = array(
      '#title' => t('Theme Engines'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#value' => sitedoc_get_system('theme_engine'),
    );  
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";
   }

  // Get vocabularies and their definitions.
  if ($sitedoc_settings['vocabulary_section']) {
    $output .= '<div class="sitedoc_vocabularies">';
    $fieldset = array(
      '#title' => t('Vocabularies'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => sitedoc_get_vocabularies(),
    );  
    $output .=  theme('fieldset', $fieldset);

     // do we want to check for orphans in the Term_node table?
    if ($sitedoc_settings['orphan_term_node']) {
      $orphans = sitedoc_check_orphan_term_node($sitedoc_settings['delete_orphan_term_nodes']);
      $fieldset = array(
        '#title' => t('Orphan Term_nodes'),
        '#collapsible' => TRUE,
        '#collapsed' => ($orphans == '<p>'. t('No orphan term_nodes found.') .'</p>'),
        '#value' => $orphans,
      );  
    $output .=  theme('fieldset', $fieldset);
     }
    $output .= "</div>\n";
   }

  // Get system variable info from cache.
  if ($sitedoc_settings['variables_section']) {
    $output .= '<div class="sitedoc_variables">';
    $fieldset = array(
      '#title' => t('Variables'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => sitedoc_get_variables(),
    );  
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";
   }

  // Get blocks info from Blocks table.
  if ($sitedoc_settings['block_section']) {  /* controls block and boxes */
    $output .= '<div class="sitedoc_blocks">';
      // use the setting to decide on a warning for missing themes
    $fieldset = array(
      '#title' => t('Blocks'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => sitedoc_get_blocks($sitedoc_settings['block_warn'], $sitedoc_settings['block_delete']),
    );  
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";

    // Get boxes (custom blocks) info from Boxes table.
    // Runs only if the blocks section is selected.
    $output .= '<div class="sitedoc_boxes">';
    $boxes = sitedoc_get_boxes($sitedoc_settings['block_warn'], $sitedoc_settings['block_delete']);
    $fieldset = array(
      '#title' => t('Custom Blocks (Boxes)'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => $boxes ? $boxes : ('<p>'. t('No boxes found.') .'</p>'),
      );
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";
   }

  // Get roles info from Roles table  (how can we get a description of the roles?).
  if ($sitedoc_settings['roles_section']) {
    $output .= '<div class="sitedoc_roles">';
    $rpt .= sitedoc_get_roles($sitedoc_settings['role_perms_list'], $sitedoc_settings['role_users']);
    $fieldset = array(
      '#title' => t('Roles'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => $rpt ? $rpt : (t('No roles found.') .' <img src="/misc/watchdog-error.png" width="17" height="17">'),
      );
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";
   }

  // Get contacts info from Contacts table.
  if ($sitedoc_settings['contacts_section']) {
    $output .= '<div class="sitedoc_contacts">';
    $rpt = sitedoc_get_contacts();
    $fieldset = array(
      '#title' => t('Contacts'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => $rpt ? $rpt : t('No contacts found.'),
      );
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";
   }

  // Get info from Profile_fields table.
  if ($sitedoc_settings['profile_fields_section']) {
    $output .= '<div class="sitedoc_profile_fields">';
    $rpt = sitedoc_profile_fields();
    $fieldset = array(
      '#title' => t('Profile Fields'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => $rpt ? $rpt : ('<p>'. t('No profile fields found.') .'</p>'),
      );
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";
   }

  // Get info from URL Alias table and cross-check it.
  if ($sitedoc_settings['url_alias_section']) {
    $output .= '<div class="sitedoc_url_alias">';
    $rpt = sitedoc_url_alias();
    $fieldset = array(
      '#title' => t('URL Aliases'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => $rpt ? $rpt : t('No aliases found.'),
      );
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";
   }

  $output .= "</div>\n";

  // Get info from Filter and Filter_format tables.
  if ($sitedoc_settings['input_format_section']) {
    $output .= '<div class="sitedoc_input_format">';
    $fieldset = array(
      '#title' => t('Input Formats and Filters'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => sitedoc_filters(),
      );
    $output .=  theme('fieldset', $fieldset);
    $output .= "</div>\n";
   }

  $output .= "</div>\n";
  return $output;

}
/**
 *  Implementation of hook_cron
 *
 *  Runs the report and saves it to disk.
 **/
function sitedoc_cron() {
  // Get the module settings and verify they've been set.
  $sitedoc_settings = variable_get('sitedoc_settings', array());
  if ($sitedoc_settings == array()) {
    watchdog('Site Doc', t('It appears that you have not been to "administer >> site configuration" to set the Site Documentation settings.'), WATCHDOG_ERROR);
   }

  // If set up for manual run, then just quit now.
  $frequency = $sitedoc_settings['archive_frequency'];
  if ($frequency == 0) { return; }

  // If set up for debugging, then -1 will let us run every time.
  if ($frequency == '999999') { $frequency = -1; }

  // Determine if it's time to make another run.
  $how_long = time() - $sitedoc_settings['archive_last_run'];

  if ($how_long > $frequency) { /* longer than how long between saves? */
    // Get the file path and append a file name.
    $filename = variable_get('file_directory_path', 'files') .'/' 
                . $sitedoc_settings['archive_directory'] .'/sitedoc_'. date('Ymd_Hi') .'.html';
    $path = $filename;
    // make sure we can write to it        
    $writable = fopen($path, 'w');
    if ($writable == FALSE) {
      watchdog('Site Doc', t('Cannot write "!file" to archive directory.', array('!file' => $filename)), WATCHDOG_ERROR);
      return;
    }

    // Call the main list, format the output as a full page (without blocks),
    //  change all the relative paths to full paths, fix the page title.
    $output = theme('page', sitedoc_list(), FALSE);
    global $base_url;
    $output = str_replace('@import "/', '@import "'. $base_url .'/', $output);
    $output = str_replace('src="/', 'src="'. $base_url .'/', $output);
    $output = str_replace('<title>Run cron', '<title>Site Documentation', $output);

    // Save the file and write a message indicating whether it worked.
    $saved_as = fwrite($writable, $output);
    if ($saved_as === 0) { watchdog('Site Doc', t('Documentation file save failed.'), WATCHDOG_ERROR); }
    else { watchdog('Site Doc', t('Documentation file saved as !name', array('!name' => $filename)), WATCHDOG_NOTICE); }
    fclose($writable);

    // update last run time
    $sitedoc_settings['archive_last_run'] = time();
    variable_set('sitedoc_settings', $sitedoc_settings); 

   } /* end if how long */
}

/****************************/
/**   Callable functions   **/
/****************************/

/**
 *   Produce the basic system overview.
 *
 *  Parameters:
 *    TRUE/FALSE - whether to fix Cron stall problem if detected.
 *
 *  Return: HTML string.
 **/
function sitedoc_drupal($kill_cron=0) {
  global $base_url;
  $_abled = array(t('Disabled'), t('Enabled'));
  $_bool = array(t('False'), t('True'));
  $_noyes = array(t('No'), t('Yes'));

  $header = array(t('Item'), t('Value'), t('Operation'));
  $setting = t('Settings');
  $rows = array();

  $rows[] = array(t('Site Name'), variable_get('site_name', 'none'), l($setting, 'admin/settings/site-information'));
  $rows[] = array(t('Version'), VERSION, NULL);
  $rows[] = array(t('Configuration file'), conf_path() .'/settings.php', NULL);
  $rows[] = array(t('Install profile'), variable_get('install_profile', 'default'), NULL);
  $rows[] = array(t('Base URL'), $base_url, NULL);

  $cache = variable_get('cache', 0);
  $cache_type = array(t('Disabled'), t('Normal'), t('Agressive'));
  $rows[] = array(t('Cache'), $cache_type[$cache], l($setting, 'admin/settings/performance'));
  $rows[] = array(t('CSS preprocess'), variable_get('preprocess_css', FALSE) ? t('Enabled') : t('Disabled'), l($setting, 'admin/settings/performance'));

  $clean_urls = variable_get('clean_url', FALSE);
  $rows[] = array(t('Clean URLS'), $_abled[$clean_urls], l($setting, 'admin/settings/clean-urls'));

  $cron_last = variable_get('cron_last', NULL);
  $cron_when = isset($cron_last) ? t('Last run !time ago', array('!time' => format_interval(time() - $cron_last))) : t('Not run yet');
  // If it last ran more than an hour ago, add "Run now" link.
  if (time() - $cron_last > 3600) { $cron_op .= l(t('Run now'), 'admin/logs/status/run-cron'); }
  else { $cron_op = NULL; } 
  $cron_semaphore = variable_get('cron_semaphore', NULL);  /* is it running now? */
  if (isset($cron_semaphore)) { 
    $how_long = time() - $cron_semaphore;  /* how long it's been running (secs) */
    $cron_when .= ' - '. t('Running for !time', array('!time' => format_interval($how_long)))
                  .'<img src="/misc/watchdog-warning.png" width="17" height="17">';
    if ($kill_cron) {
      if ($kill_cron <= $how_long) {
        variable_del('cron_semaphore');  /* turn off "cron started" flag */
        variable_del('cron_last');       /** I don't think this is actually needed **/
       } /* end if how long */
     }  /* end if kill_cron */
   }  /* end if semaphore */
  $rows[] = array(t('Cron'), $cron_when, $cron_op);

  if (module_exists('search')) {
    $rows[] = array(t('Search Cron limit'),
                    variable_get("search_cron_limit", null),
                    l($setting, 'admin/settings/search'));
    $remaining = 0;
    $total = 0;
    foreach (module_list() as $module) {
      if (module_hook($module, 'search')) {
        $status = module_invoke($module, 'search', 'status');
        $remaining += $status['remaining'];
        $total += $status['total'];
      }
    }
    $count = format_plural($remaining, 'There is 1 item left to index.', 'There are @count items left to index.');
    $percentage = ((int)min(100, 100 * ($total - $remaining) / max(1, $total))) .'%';
    $status = t('%percentage of the site has been indexed.', array('%percentage' => $percentage)) .' '. $count;
    $link = l($setting, 'admin/settings/search');
    if ($remaining) { $link .= ', '. l(t('Run Cron'), 'admin/logs/status/run-cron'); }
    $rows[] = array(t('Search status'), $status, $link);
  }

  $rows[] = array(t('Anonymous'), variable_get('anonymous', 'anonymous'), l($setting, 'admin/settings/site-information')); 
  $rows[] = array(t('File directory'), variable_get('file_directory_path', 'files/'), l($setting, 'admin/settings/file-system'));

  if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PUBLIC) {
    $downloads = t('Public'); 
    $download_op = NULL;
   }
  else {
    $downloads = t('Private') .'<img src="/misc/watchdog-warning.png" width="17" height="17">'; 
    $download_op = l($setting, 'admin/settings/file-system');
   }  

  $rows[] = array(t('Download method'), $downloads, $download_op);

  // Is the menu choice restricted?
  $menu_restrict = variable_get('menu_parent_items', NULL);  
  // "Show all menus" is Zero. Unless the setting has been changed it won't be present (NULL)
  if ($menu_restrict) {
    $menu_msg = t('Restricted') .' ('. $menu_restrict .') <img src="/misc/watchdog-warning.png" width="17" height="17">';
   }
  else {
    $menu_msg = t('Show all menus') .'<img src="/misc/watchdog-ok.png" width="17" height="17">';
   }
  $rows[] = array(t('Content menu links'), $menu_msg, l($setting, 'admin/build/menu/settings'));

  $rows[] = array(t('Default theme'), variable_get('theme_default', 'standard'), l($setting, 'admin/build/themes'));
  
  $teaser_len = variable_get('teaser_length', 300);
  $rows[] = array(t('Teaser length'), $teaser_len ? $teaser_len : t('unlimited'), l($setting, 'admin/content/node-settings'));
  
  // Get default filter and then look up its name.
  $filter = variable_get('filter_default_format', 0);
  $filter_name = db_fetch_object(db_query('SELECT f.name FROM {filter_formats} f WHERE f.format=\'%d\' LIMIT 1', $filter));
  if ($filter == 1) {
    $filter_warn = NULL;
    $filter_op = NULL;
   }
  else {
    $filter_warn = '<img src="/misc/watchdog-warning.png" width="17" height="17"> ';
    $filter_op = l($setting, 'admin/settings/filters');
   }  
  $rows[] = array(t('Default filter format'), $filter_name->name . $filter_warn, $filter_op);

  $rows[] = array(t('Operating system'), php_uname('s'), NULL);

  $fieldset = array(
    '#title' => t('Drupal'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
    '#value' => theme('table', $header, $rows) ."\n",
  );
  $output .=  theme('fieldset', $fieldset);

  // Webserver information.
  $rows = array();

  $rows[] = array(t('Web server'), $_SERVER['SERVER_SOFTWARE'], NULL);
  if (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== FALSE) { /* is it Apache? */
    $rows[] = array(t('Server name'), $_SERVER['SERVER_NAME'], NULL);
    if (function_exists('apache_get_modules')) { /* can we get a list of modules ? */
      $rows[] = array(t('Apache modules'), implode(', ', apache_get_modules()), NULL);
     } /* end if function */
   } /* end if apache */

  $fieldset = array(
    '#title' => t('Webserver Information'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#value' => theme('table', $header, $rows) ."\n",
  );
  $output .=  theme('fieldset', $fieldset);
  
  // Php info.
  $rows = array();
  $error_names = array(
    'E_ERROR' => 1,
    'E_WARNING' => 2,
    'E_PARSE' => 4,
    'E_NOTICE' => 8,
    'E_CORE_ERROR' => 16,
    'E_CORE_WARNING' => 32,
    'E_COMPILE_ERROR' => 64,
    'E_COMPILE_WARNING' => 128,
    'E_USER_ERROR' => 256,
    'E_USER_WARNING' => 512,
    'E_USER_NOTICE' => 1024,
    'E_STRICT' => 2048,
    'E_RECOVERABLE_ERROR' => 4096,
    );
  
  list($php_vers, $php_rel, $php_fix) = explode('.', phpversion());

  $rows[] = array(t('Php version'), phpversion(), l(t('View phpinfo'), 'sitedoc/phpinfo'));
  if ($php_vers < 6) {
    $safe_mode = ini_get('safe_mode');
    if (is_bool($safe_mode)) { $$safe_mode = $_abled[$safe_mode]; }
    if (empty($safe_mode)) { $safe_mode = $_abled[0]; }
    $rows[] = array(t('Safe mode'), $safe_mode, NULL);
  }

  $disperr = ini_get('display_errors');
  if (is_bool($disperr)) { $disperr = $_noyes[$disperr]; }
  if (empty($disperr)) { $disperr = $_noyes[0]; }
  $rows[] = array(t('Display errors'), $disperr, NULL);

  $mem_limit = ini_get('memory_limit');
  $rows[] = array(t('Memory limit'), $mem_limit ? $mem_limit : t('default') .' <img src="/misc/watchdog-warning.png" width="17" height="17">', NULL);

  // Changing the error level returns the previous level, so we'll turn errors off then set it back as it was.
  $current = ini_get('error_reporting');

  $levels = array();
  if ($current == E_ALL) { $levels[] = 'E_ALL'; }
  else {
    foreach ($error_names as $name => $bit) {
      if ($current && $bit) { $levels[] = $name; }
    }
  }
  $rows[] = array(t('Error reporting'), implode(' + ', $levels), NULL);

  $rows[] = array(t('Upload max filesize'), ini_get('upload_max_filesize'), NULL);
  $rows[] = array(t('Magic quotes GPC'), $_abled[ini_get('magic_quotes_gpc')], NULL);
  $rows[] = array(t('Magic quotes runtime'), $_abled[ini_get('magic_quotes_runtime')], NULL);
  $rows[] = array(t('Precision'), ini_get('precision'), NULL);
  $rows[] = array(t('Register globals'), $_abled[ini_get('register_globals')], NULL);
  $rows[] = array(t('Session cache limiter'), ini_get('session.cache_limiter'), NULL);
  $cookie_params = session_get_cookie_params();
  $rows[] = array(t('Session cookie domain'), !empty($cookie_params['domain']) ? $cookie_params['domain'] : t('- none -'), NULL);
  $rows[] = array(t('Session name'), session_name(), NULL);
  $rows[] = array(t('Session save handler'), ini_get('session.save_handler'), NULL);

   /* php extensions */
  if (extension_loaded('gd')) {
    $gd = gd_info();
    $rows[] = array(t('GD version'),
                    $gd['GD Version'] .'<br />'
                    . t('FreeType support') .' ('. $_abled[$gd['FreeType Support']] .'), '
                    . t('Jpg support') .' ('. $_abled[$gd['JPG Support']] .'), '
                    . t('Png support') .' ('. $_abled[$gd['PNG Support']] .')',
                    NULL,
               );
   }
  else { $rows[] = array(t('GD support'), t('Disabled')); }

  if (extension_loaded('curl')) {
    $curl = curl_version();
    $rows[] = array(t('CURL version'), $curl['version'], NULL);
  }
  else {
    $rows[] = array(t('CURL support'), t('Disabled'), NULL);
  }

  $rows[] = array(t('Multibyte support'), $_abled[extension_loaded('mbstring')], NULL);
  $overload = ini_get('mbstring.func_overload');
  $oload = array();
  if ($overload & 1) { $oload[] = t('mail'); }
  if ($overload & 2) { $oload[] = t('string'); }
  if ($overload & 4) { $oload[] = t('regex'); }
  $rows[] = array('Mbstring.func_overload', (empty($oload) ? t('disabled') : implode(', ', $oload)), NULL);
  $rows[] = array(t('XML support'), $_abled[extension_loaded('xml')], NULL);
  $rows[] = array(t('Zip support'), $_abled[extension_loaded('zip')], NULL);
  $rows[] = array(t('Zlib support'), $_abled[extension_loaded('zlib')], NULL);

  $fieldset = array(
    '#title' => t('PHP Information'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#value' => theme('table', $header, $rows) ."\n",
  );
  $output .=  theme('fieldset', $fieldset);

  return $output;
}

/**
 *   Menu callback to produce PHPINFO page.
 **/
function sitedoc_phpinfo() {
  phpinfo(INFO_GENERAL | INFO_CONFIGURATION | INFO_ENVIRONMENT);
  return;
}

/**
 *   Produce the database tables overview and give overall size info
 *
 *  Parameters:
 *    none
 *
 *  Return: HTML string
 **/
function sitedoc_database_overview($optimize_tables=FALSE, $show_index=FALSE) {
  global $base_url, $db_prefix, $db_url;

// $db_url may be an array if using multiple databases.
  if (is_array($db_url)) {
    foreach ($db_url as $key => $value) {
    $dbname = ucfirst($key);
       // Switch databases
    db_set_active($key);
    $fieldset = array(
      '#title' => $dbname,
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#value' => _sitedoc_database($dbname, $value, $optimize_tables, $show_index),
      );
    $output .=  theme('fieldset', $fieldset);
    }
     // Return to default database
    db_set_active('default');
  }
  else {
    $fieldset = array(
      '#title' => t('Database Information'),
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#value' => _sitedoc_database('database', $db_url, $optimize_tables, $show_index),
      );
    $output .=  theme('fieldset', $fieldset);
  }
  return $output;
}

// Format the database information.
// This was split out when I was informed that the $db_url variable could be an array.
function _sitedoc_database($actdb, $db_url, $optimize_tables=FALSE, $show_index=FALSE) {
  global $base_url, $db_prefix;
  $rows = array();
  $header = array();

  $db_types = array('mysql' => 'MySQL',
                    'mysqli' => 'MySQLi',
                    'postgres' => 'Postgres',
                );
        
  $output .= '<table cellpadding="5"><tr>'."\n";        

  $output .= '<td valign="top">'."\n";
  $output .= '<h5>'. t('Overview') ."</h5>\n";
  $database_type = $db_types[$GLOBALS['db_type']];
  $database_version = db_version();
  $rows[] = array(t('Database type'), isset($database_type) ? $database_type : t('Unknown'));
  $vers = isset($database_version) ? $database_version : t('Unknown');
  $rows[] = array(t('Version'), l($vers, 'admin/logs/status/sql'));

  $db = parse_url($db_url);
// Don't show password
  $db['pass'] = str_repeat('&bull;', strlen($db['pass']));
  $dbrows = array();
  foreach ($db as $key => $value) {
    $dbrows[] = array($key, $value);
  }
  $rows[] = array(t('Database URL'), theme('table', NULL, $dbrows));

  $rows[] = array(t('Base URL'), $base_url);
  $rows[] = array(t('Database prefix'), empty($db_prefix) ? '- none -' : $db_prefix);
  $grants = db_result(db_query('SHOW GRANTS'));
  $privileges = trim(str_replace('GRANT', ' ', substr($grants, 0, strpos($grants, ' ON'))));
  $rows[] = array(t('Privileges'), $privileges);

  $output .= theme('table', $header, $rows) ."\n";
  $output .= "</td>\n";

// Check if we have a version of MySql that supports the variable and status checks
// TODO: Can Postgres do something similar?
  if (('MySQL' == $database_type && version_compare($database_version, '4.1.0', '>=')) || 'MySQLi' == $database_type) {
  
    // The next two things are done as lists to 
    //   a) simplify building the query,
    //   b) create the possibility of future settings page selection.

    // Get selected variables from the database.
    $variables_list = array('character_set_database',
                            'character_set_results',
                            'collation_connection',
                            'collation_database',
                            'collation_server',
                            'have_dynamic_loading',
                            'have_innodb',
                            'have_isam',
                            'have_query_cache',
                            'have_raid',
                            'max_connections',
                            'query_cache_size',
                            'query_cache_type',
                         );
    $output .= _sitedoc_dbvars_list('VARIABLES', t('Selected Variables'), $variables_list, $database_version);
  
    // Get selected status information from the database.
    $variables_list = array('Qcache_free_memory',
                            'Qcache_hits',
                            'Qcache_not_cached',
                            'Qcache_queries_in_cache',
                            'Max_used_connections',
                            'Threads_running',
                            'Threads_cached',
                            'Threads_connected',
                            'Threads_created',
                         );
    $output .= _sitedoc_dbvars_list('STATUS', t('Selected Status'), $variables_list, $database_version);

    $output .= "</tr></table>\n";
  } /* end MySql check */
  
//  $output .= '<h5>'. t('Table Status') ."</h5>\n";

  // Do the table status section.
  $result = db_query("SHOW TABLE STATUS");

 // I used a class to align fields rather than 'align' because the some themes override 'align' in headers.
  $statrpt = "\n<table><thead><tr>";
  $statrpt .= '<th>'. t('Table') .'</th>'
    .'<th>'. t('Engine') .'</th>'
    .'<th>'. t('Version') .'</th>'
    .'<th>'. t('Row Format') .'</th>'
    .'<th class="sitedoc_right">'. t('Rows') .'</th>'
    .'<th class="sitedoc_right">'. t('Data Length') .'</th>'
    .'<th class="sitedoc_right">'. t('Index Length') .'</th>'
    .'<th class="sitedoc_right">'. t('Overhead') .'</th>'
    .'<th class="sitedoc_right">'. t('Operation') .'</th>'
    .'</tr></thead><tbody>';
  $datalen = 0;
  $indexlen = 0;
  $overhead = 0;
  $row_count = 0;
  $rows = 0;
  while ($table = db_fetch_array($result)) {
    ++$rows;
    if ($rows % 2) { $row_class = 'odd'; }
    else { $row_class = 'even'; }
    $row_count += $table['Rows'];
    $datalen += $table['Data_length'];
    $indexlen += $table['Index_length'];
    $overhead += $table['Data_free'];
    $r = number_format($table['Rows']);       
    $d = number_format($table['Data_length']);
    $i = number_format($table['Index_length']);
    if ($table['Data_free']) { /* is there some overhead? */
      $o = number_format($table['Data_free']);
      if ($optimize_tables) { /* do we want to release it?*/
        $worked = db_query('OPTIMIZE TABLE {%s}', $table['Name']);
        if ($worked) { $o .= '<br />'. t('released'); }
       } /* end release */
     } /* end overhead */
    else { $o = NULL; }
    $statrpt .= "\n".'<tr class="'. $row_class .'">'
      .'<td>'. $table['Name'] .'</td>'
      .'<td>'. $table['Engine'] .'</td>'
      .'<td align="center">'. $table['Version'] .'</td>'
      .'<td>'. $table['Row_format'] .'</td>'
      .'<td class="sitedoc_right">'. $r .'</td>'
      .'<td class="sitedoc_right">'. $d .'</td>'
      .'<td class="sitedoc_right">'. $i .'</td>'
      .'<td class="sitedoc_right">'. ($table['Data_free'] ? $o : '') .'</td>'
      .'<td>'. l(t('Show contents'), 'sitedoc/table/'. $table['Name'], array('title' => t("Display the contents of the '@table' table.", array('@table' => $table['Name'])))) .'</td>'
      . (empty($table['Comment']) ? NULL : '</tr><tr class="'. $row_class .'"><td></td><td colspan="10"><img src="/misc/menu-collapsed.png" width="7" height="7"> <em>'. $table['Comment'] .'</em></td>');
    if ($show_index) {
      $statrpt .= '</tr><tr class="'. $row_class .'"><td></td><td valign="top" align="right"><em>'. t('Indexes') .':</em></td><td colspan="10">'. _sitedoc_show_index($table['Name']) .'</td>';
    }
    $statrpt .= '</tr>';
  } /* end while */
  $statrpt .= '<tr><td colspan="4">&nbsp;</td>'
             .'<td class="sitedoc_right">----------</td>' 
             .'<td class="sitedoc_right">----------</td>' 
             .'<td class="sitedoc_right">----------</td>' 
             .'<td class="sitedoc_right">----------</td>'
             .'</tr>';
  $statrpt .= '<tr><td colspan="4"><em>total</em></td>'
             .'<td class="sitedoc_right">'. number_format($row_count) .'</td>'
             .'<td class="sitedoc_right">'. number_format($datalen) .'</td>'
             .'<td class="sitedoc_right">'. number_format($indexlen) .'</td>'
             .'<td class="sitedoc_right">'. number_format($overhead) .'</td>'
             .'</tr>';
  $statrpt .=  "\n</table><p>";

  $fieldset = array(
    '#title' => t('Table Status'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE,
    '#value' => $statrpt,
    );
  $output .=  theme('fieldset', $fieldset);

  $totsize = $datalen + $indexlen;
  $kb = $totsize / 1024;
  $mb = $kb / 1024;
  $gb = $mb / 1024;
  $units = 'KB';
  $howmany = $kb;
  if ($mb >= 1) {
    if ($gb >= 1) { $units = 'GB'; $howmany = $gb; }
    else  { $units = 'MB'; $howmany = $mb; }
   }
 
  $output .= t('Total database size is !num !unit', array('!num' => sprintf('%.1f', $howmany), '!unit' => $units)) .'</p>';
  drupal_set_message(t('!count tables found in !db.', array('!count' => $rows, '!db' => $actdb)), 'notice');

  return $output ."\n";
}

/*
 *  Helper function to show the index structure of a database table.
 */ 
function _sitedoc_show_index($table_name) {
  $output = null;
  $index_list = db_query('SHOW INDEX FROM '. $table_name);
  $indices = array();
  while ($index = db_fetch_array($index_list)) {
    if ($index['Seq_in_index'] == 1) {        
      $indices[$index['Key_name']] = array(
         'name' => $index['Column_name'] . ($index['Sub_part'] ? ' ('. $index['Sub_part'] .')' : null),
         'collation' => $index['Collation'],
         'count' => $index['Cardinality'],
         );
    }
    else { $indices[$index['Key_name']]['name'] .= ', '. $index['Column_name'] . ($index['Sub_part'] ? ' ('. $index['Sub_part'] .')' : null);
    }
  }
  if (count($indices)) {
    $rows = array();
    $header = array(t('Key Name'), t('Columns'), t('Collation'), t('Cardinality'));
    foreach ($indices as $key_name => $values) {
      $rows[] = array($key_name,
         $values['name'],
         array('data' => $values['collation'], 'align' => 'center'),
         array('data' => $values['count'], 'align' => 'center')
         );
    }
    return theme('table', $header, $rows);
  }
  else { return '<span class="admin-missing">No index found.</span> <img src="/misc/watchdog-warning.png" width="17" height="17">'; }
}

/**
 *   Helper function for database variable list
 */
function _sitedoc_dbvars_list($type, $title, $variables_list, $db_vers) {
  if (count($variables_list)) {
    $rows = array();
    $output .= '<td valign="top">'."\n";
    $output .= '<h5>'. $title ."</h5>\n";
  $show = 'SHOW '. $type;
  $vers = explode('.', $db_vers);
  switch ($vers[0]) {
    case 4:
      foreach ($variables_list as $key => $name) {
          $list = $show ." LIKE '". $name ."'";
          $result = db_query($list);
          while ($data = db_fetch_array($result)) {
            $rows[] = array($data['Variable_name'], $data['Value']);
          }
    } /* end foreach */
        $output .= theme('table', $header, $rows) ."\n";
        $output .= "</td>\n"; 
    break;

    case 5:
        $list = $show ." WHERE Variable_Name LIKE '". implode("' OR Variable_Name LIKE '", $variables_list) ."'";
        $result = db_query($list);
        while ($data = db_fetch_array($result)) {
          $rows[] = array($data['Variable_name'], $data['Value']);
        }
        $output .= theme('table', $header, $rows) ."\n";
        $output .= "</td>\n"; 
    break;
    
      default:
      drupal_set_message('Site Documentation: Unknown database version'. $vers[0], 'warning');    
  
  } /* end switch */
  } /* end count variables */
  return $output;
}

/**
 *   Function to display table contents.
 *
 *   @param:
 *     $table - table name to show.
 *     $rows_per_page - how many rows to format per page. Defaults to 20.
 *
 *   @return: HTML string
 */
function sitedoc_show_table($table = null, $rows_per_page = 20) {
  if (!$table || !db_table_exists($table)) {
    drupal_set_message(t('You must supply a valid database table name.'), 'error');
    drupal_access_denied();
  }

  // We get the first (or only) part of the Primary key to be added to the sort sequence.
  $result = db_query("SHOW INDEX FROM {$table}");
  $x = db_fetch_array($result);
  if ($x === false) {
    drupal_set_message(t("The '@table' table has no index defined. This is probably normal.", array('@table' => $table)), 'notice');
    $first_key = null;
  }
  else {
    $first_key = $x['Column_name'];
  } 

  drupal_set_title(t('@table Table Contents', array('@table' => ucwords($table))));
  $output = '<p>'. t('Click on a column title to sort by that column.') .'</p><br/>';
  $rows = array();

  // Now we get the column names from the table and build the header.
  $header = array();
  $result = db_query("SHOW COLUMNS FROM {$table}");

  while ($col_desc = db_fetch_array($result)) {
    $header[] = array(
      'data' => ucwords(str_replace('_', ' ', $col_desc['Field'])),
      'field' => '`'. $col_desc['Field'] .'`',
      );
  }
  
  // Get the data rows from the table.
  $select = "SELECT * FROM {$table}";
  // Set it up so that the user can sort on any column, but the primary key will always be the last value to sort on.
  $select .= tablesort_sql($header) . ($first_key ? (', '. $first_key .' ASC') : null);
  // Do the query so that we can page the data.
  $result = pager_query($select, $rows_per_page);

  while ($row = db_fetch_array($result)) {
    $line = array();
    foreach ($row as $key => $value) {
      // We use check_markup to apply our filters.
      $line[] = check_markup($value, FILTER_FORMAT_DEFAULT, true);
    }
    $rows[] = $line;
  }

  // Build the displayable table.
  $output .= theme('table', $header, $rows);
  $output .= theme('pager', $rows_per_page);
  return $output;
}

/**
 *   Produce the modules list.
 *
 *  Parameters:
 *    none
 *
 *  Return: HTML string
 **/
function sitedoc_get_modules($exclude_disabled=FALSE, $sort_order=0) {
  $output = '';
  $rows = array();

  $module = array();
  $files = module_rebuild_cache();
  $sortkey = array();
  // Set some default package stuff for Drupal.
  $package_list = array('Core - required', 'Core - optional');
  $package_path = array('modules/ ', 'modules/ ');

  foreach ($files as $filename => $file) {
    $name = $file->info['name'];
    $level = NULL;
    
    $package = empty($file->info['package']) ? t('Other') : ucfirst($file->info['package']);
    $project = empty($file->info['project']) ? ucfirst($file->name) : ucfirst($file->info['project']);
    $key = array(NULL, NULL, $name);

    // The filename is something like sites/all/modules/package/contrib/project/project.module.
    // For the path sorting option, we want the part before "package" as key[0],
    // and the part after "package" ("contrib/") in key[1].
    // "project.module" is in 'basename' already.
    // So let's start by breaking the filename up into pieces.
    $pieces = explode('/', $file->filename);
    // We don't need the last array element (project.module).
    unset($pieces[count($pieces) - 1]);

    // Check for what kind of package it is.
    if ($package == t('Other') || $project == 'Drupal') {
      // 'Special' packages. Remove the last piece ("project/").
      unset($pieces[count($pieces) - 1]);
      // We add a blank to the end because 'sort' acts funny.
      $key[0] = implode('/', $pieces) .'/ ';
    }
    else {
      // 'Normal' packages.
      $where = array_search($package, $pieces);
      if ($where === FALSE) {
        // If "package" is not part of the name, just pull the "project" part off.
        unset($pieces[count($pieces) - 1]);
        $key[0] = implode('/', $pieces) .'/ ';
      }
      else {
        // "Package" is part of the name, split the keys up.
        $key[0] = implode('/', array_slice($pieces, 0, $where)) .'/ ';
        $key[1] = implode('/', array_slice($pieces, $where + 1)) .'/';
      }
    }

    $where = array_search($package, $package_list);
    if ($where === FALSE) {
      $package_list[] = $package;
      $package_path[] = $key[0];
    }
    else {
      // Package already in list.
      if ($package_path[$where] != $key[0] && $package != t('Other')) {
        // The path is different from what we found earlier.
        // One may be a substring of the other - that's okay, but save the shorter one.
        if ((strpos($package_path[$where], rtrim($key[0])) === FALSE && strpos($key[0], rtrim($package_path[$where])) === FALSE) || ($project == 'Drupal')) {
          if ($project == 'Drupal') { $level = 'error'; }
          else { $level = 'warning'; }
          drupal_set_message(t("'!name' for the '!pkg' package was found in '!found' but a previous module was in '!prior'.", array('!name' => $name, '!pkg' => $package, '!found' => $key[0], '!prior' => $package_path[$where])), $level);
        }
        else {
          // Resave the shorter one.
          if (strlen($package_path[$where]) < strlen($key[0])) {
            $package_path[$where] = $key[0];
          }
        }
      }
    }

    $module[$name] = array(
      'version' => $file->info['version'],
      'basename' => $file->basename,
      'filename' => $file->filename,
      'path' => $key[0],
      'contrib_path' => $key[1],
      'package' => $package,
      'project' => $project,
      'dependencies' => $file->info['dependencies'],
      'dependents' => $file->info['dependents'],
      'description' => $file->info['description'],
      'status' => $file->status,
      'level' => $level,
      );

    // Build a parallel array to sort with.
    $separator = '|';
    switch ($sort_order) {
      case 0:  // package, project, module
        $key = array($module[$name]['package'], $module[$name]['project'], $name);
        break;

      case 1:  // path, module
        // Keys already set.
        break;

      default:  // This should never happen.
        drupal_set_message(t('Unrecognized sort order.'), 'error');
        $key = array($module[$name]['package'], $module[$name]['project'], $name);
    }
    $sortkey[] = implode('|', $key);

    $dependencies = array();
    // Check for missing dependencies.
    if (is_array($file->info['dependencies'])) {
      foreach ($file->info['dependencies'] as $dependency) {
        if (!isset($files[$dependency]) || !$files[$dependency]->status) {
          if (isset($files[$dependency])) {
            $dependencies[] = $files[$dependency]->info['name'] .'<span class="admin-disabled">('. t('disabled') .')</span>';
           }
          else {
            $dependencies[] = drupal_ucfirst($dependency) .'<span class="admin-missing">('. t('missing') .')</span>';
            $disabled[] = $filename;
           }
        } /* end if !isset */
        else {
          $dependencies[] = $files[$dependency]->info['name'] .'<span class="admin-enabled">('. t('enabled') .')</span>';
         }
      }  /* end foreach depend */
     } /* end if isarray */   

      // Un-array dependencies.
    if (!empty($dependencies)) { $module[$name]['dependencies'] = implode(', ', $dependencies); }

    // Un-array enabled dependents.
    if (!empty($module[$name]['dependents'])) { $module[$name]['required'] = implode(', ', $module[$name]['dependents']); }
   } /* end foreach files */

  $header = array(
    t('Name'),
    t('Version'),
    t('Status'),
    t('Project / Package'),
    t('Description'),
    t('Required By'),
    t('Depends On'),
    ); 
  drupal_set_message(t('!count modules found.', array('!count' => count($module))), 'notice');
  $disabled_count = 0;
  $previous = NULL;

  // Order it by project, package, module name.
  sort($sortkey);

  foreach ($sortkey as $key => $value) {
    $keys = explode('|', $value);
    $name = $keys[count($keys) - 1];

    if ($keys[0] != $previous) {
      if (count($rows) > 0) {
        $fieldset = array(
          '#title' => $previous,
          '#collapsible' => TRUE,
          '#collapsed' => TRUE,
          '#value' => theme('table', $header, $rows),
          );
        $output .=  theme('fieldset', $fieldset);
      }
      $previous = $keys[0];
      $rows = array();      
    } /* end key != previous */

    if (!$exclude_disabled || $module[$name]['status']) {
      $path = $module[$name]['path'];
      if ($module[$name]['level']) {
      $path = '<span class="admin-missing">'. $path .'</span>';
      }
      $rows[] = array(
        $name,
        $module[$name]['version'],
        $module[$name]['status'] ? 'Enabled' : 'Disabled',
        '<small>'. $module[$name]['project'] .'<br />'. $module[$name]['package'] .'<br />'. $path .'</small>',
        '<small>'. $module[$name]['description'] .'</small>',
        $module[$name]['required'],
        $module[$name]['dependencies'],
        );
    } // end if exclude disabled
    else { ++$disabled_count; }

  } /* end foreach sortkey */
  // Flush the last set.
  if (count($rows) > 0) {
    $fieldset = array(
      '#title' => $previous,
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#value' => theme('table', $header, $rows),
      );
    $output .=  theme('fieldset', $fieldset);
  }

  if ($exclude_disabled) {
    $output .= '<p>'. t('!count disabled modules excluded.', array('!count' => $disabled_count)) .'</p>';
  }
  return $output ."\n";
}

/**
 *   Produce the Themes and Theme_engine lists
 *
 *  Parameters:
 *    Which type of row to return
 *      theme  - get themes list
 *      theme_engine  - get theme engines list
 *
 *  Return: HTML string
 **/
function sitedoc_get_system($type=null) {
  if (!$type) { return '<p>'. t('No system table type specified.') .'</p>'; }
  $status = array('Disabled', 'Enabled');

  $string = ucfirst(strtolower($type));
//  $output = "<h3>$string</h3>";

   // This ORDER BY makes the list come out by directory the entry comes from.
  $result = db_query('SELECT s.name, s.filename, s.status, s.description FROM {system} s WHERE s.type=\'%s\' ORDER BY s.status DESC, s.name ASC', $type);
  drupal_set_message(db_num_rows($result) .' '. $string . t(' found.'), 'notice');
  if (db_num_rows($result)) {
    $header = array($string, t('Path'), t('Status'), t('Description'));
    $rows = array();
    while ($mod = db_fetch_array($result)) {
      $rows[] = array(
        ucfirst($mod['name']),
        $mod['filename'],
        $status[$mod['status']],
        $mod['description']
        );
     } /* end while */
    $output .=  theme('table', $header, $rows);
   } /* end if */
  else { $output .= t('No entries found! Very curious.'); }
  return $output ."\n";
}

/**
 *   Produce the blocks list
 *
 *  Parameters:
 *    $warn - TRUE / FALSE to produce orphan warning message.
 *    $delete - TRUE / FALSE to delete orphans.
 *
 *  Return: HTML string
 **/
function sitedoc_get_blocks($warn=TRUE, $delete=FALSE) {
  $status = array();
  $status = array(t('disabled'), t('enabled'));
  $viz = array(t('Show except'), t('Show only'), t('Php'));
  $del = NULL;
  
  // Check if the table is missing an index.
  $indexes = db_fetch_array(db_query('SHOW INDEX FROM {blocks}'));
  if ($indexes == FALSE) {
    $index_msg = ' '. t('Performance may be improved by adding an index. See <a href="!url">this discussion</a>.', array('!url' => 'http://drupal.org/node/164532'));
    $msg_level = 'warning';
  }
  else {
    $index_msg = NULL;
    $msg_level = 'notice';
  }

  // Get blocks from the Blocks table.
  $result = db_query('SELECT b.module, b.delta, b.theme, b.status, b.weight, b.region, b.custom, b.throttle, b.visibility, b.pages, b.title FROM {blocks} b ORDER BY b.theme, b.module, b.delta');

  $header = array(
    t('Theme'), 
    t('Module'), 
    array('data' => t('Delta'), 'align' => 'center'), 
    t('Name') .' /<br/>'. t('Title'),
    t('Status'), 
    array('data' => t('Weight'), 'class' => 'sitedoc_right'),
    t('Region'),
    t('Roles'),
    array('data' => t('Visibility'), 'align' => 'center'),
    t('Pages'),
    );
  $output = '<h3>'. t('Blocks') .'</h3>';

  $rows = array();
  $missing_theme = FALSE;

  while ($blk = db_fetch_object($result)) {
    $theme = $blk->theme;
    $thmq = db_query("SELECT s.status FROM {system} s WHERE s.type='theme' AND s.name='%s' LIMIT 1", $theme);
    if (db_num_rows($thmq)) { 
      $thm = db_fetch_array($thmq);
      $theme .= ' <span class="admin-'. $status[$thm['status']] .'">('. ucfirst($status[$thm['status']]) .')</span>';
    } // End db_num_rows($thmq).
    else {
      $theme .= ' <span class="admin-missing">('. t('missing') .')</span>';
      $missing_theme = TRUE;
      $mod_delta = "module='$blk->module' AND delta='$blk->delta'";
      $delblk = "DELETE FROM {blocks} WHERE theme='$blk->theme' AND $mod_delta LIMIT 1; ";
      if ($has_roles = db_query("SELECT rid FROM {blocks_roles} WHERE $mod_delta LIMIT 1")) {
        $delrole = "DELETE FROM {blocks_roles} WHERE $mod_delta LIMIT 1; ";
      }
      else { $delrole = null; /*}
      else {*/ drupal_set_message("has_roles query returned $has_roles"); }
      if ($delete) { 
        $deleted = db_query($delblk) && ($delrole ? db_query($delrole) : true);
        if ($deleted == false) {
          $theme .= ' <span class="admin-missing">('. t('NOT DELETED') .')</span>';
        }
        else {
          $theme .= ' <span class="admin-disabled">('. t('DELETED') .')</span>';
          drupal_set_message(t('Deleted block ') . $blk->theme .'/'. $blk->module .'/'. $blk->delta, 'notice');
        }
      }
      else { $del .= $delblk . $delrole; }
     }
     
    $blocks = module_invoke($blk->module, 'block', 'list');  // Get block information from module.

    $w = $blk->weight ? $blk->weight : '';    // Don't show 0 weight.
    $n = $blocks[$blk->delta]['info'];  // Get block name. 
    // If there is one, put title on next line.
    if (!empty($blk->title)) { $n .= ' /<br />'. $blk->title; } 
     // The pages field format depends on the visibility setting
     // 2 is php, 0 or 1 is pages list
    if ($blk->visibility < 2) { 
      $pt = trim(check_plain($blk->pages));
      if (strcmp($pt, '') == 0) { $pgs = NULL; }  
      else { $pgs = explode("\n", $pt); 
        if (is_array($pgs)) {
          if (count($pgs) > 1) { $pgs = theme('item_list', $pgs); }
          else { $pgs = $pgs[0]; }
         }  // End is_array
       }  // End else strcmp
     }  // End visibility
  // Visibility = 2
    else { $pgs = highlight_string($blk->pages, TRUE); }
    
    // Get the roles for the blocks.
    $rids = db_query('SELECT br.rid, r.name FROM {blocks_roles} br INNER JOIN {role} r ON r.rid=br.rid WHERE br.module=\'%s\' AND br.delta=\'%d\'', $blk->module, $blk->delta);
    $blk_rids = array();
    while ($blk_role = db_fetch_array($rids)) {
      $blk_rids[] = $blk_role['name'];
     }
    
    $rows[] = array(
      $theme,
      $blk->module == 'block' ? 'block /<br/><small><em>'. t('see boxes') .'</em></small>' : $blk->module,
      array('data' => $blk->delta, 'align' => 'center'),
      $n,
      '<span class="admin-'. $status[$blk->status] .'">'. ucfirst($status[$blk->status]) .'</span>',
      array('data' => $w, 'class' => 'sitedoc_right'),
      $blk->status ? $blk->region : '',    /* no region if not enabled */
      count($blk_rids) ? '<small>'. implode(', ', $blk_rids) .'</small>' : NULL,
      array('data' => $viz[$blk->visibility], 'align' => 'center'),
      $pgs,
      );
   } /* end while */

  $count = count($rows);
  if ($count == 0) { drupal_set_message(t('No Blocks found.'), 'notice'); }
  else {
    drupal_set_message(t('!count Blocks found.', array('!count' => $count)) . $index_msg , $msg_level);
    if ($index_msg) {
      drupal_set_message(t('For example:') .' ALTER TABLE `blocks` ADD PRIMARY KEY(`theme`(32), `module`, `delta`)', $msg_level);
    }
    $output .=  theme('table', $header, $rows);
  }
  if ($missing_theme && $warn) { drupal_set_message(t('Blocks defined for non-existent theme(s)'), 'warning'); }

  if ($missing_theme && !$delete) { $output .= '<p>'. $del .'</p>'; }

  return $output ."\n";
}

/**
 *   Produce the boxes (manual blocks, additional data) list.
 *
 *  Parameters:
 *    $warn - TRUE / FALSE to produce orphan warning message.
 *    $delete - TRUE / FALSE to delete orphans.
 *
 *  Return: HTML string
 **/
function sitedoc_get_boxes($warn=TRUE, $delete=FALSE) {
  $result = db_query('SELECT * FROM {boxes} b');
  $output = NULL;
  $orphans = FALSE;
  $header = array(
    array('data' => 'Bid', 'class' => 'sitedoc_right'), 
    t('Box Name'),
    array('data' => t('Format'), 'class' => 'sitedoc_right'),
    t('Body'),
    );
  $rows = array();
  while ($box = db_fetch_array($result)) {
    $info = $box['info'];
    $check_block = db_result(db_query("SELECT COUNT(theme) FROM {blocks} WHERE module='block' AND delta=%d LIMIT 1", $box['bid']));
    if ($check_block == 0) {
      $orphans = TRUE;
      $info .= '<br/><span class="admin-missing">('. t('block missing') .')</span>';
      if ($delete) {
        $delbox = db_query('DELETE FROM {boxes} WHERE bid=%d LIMIT 1', $box['bid']);
        drupal_set_message(t('Box number !bid deleted.', array('!bid' => $box['bid'])), 'notice');
      }
    }

    $rows[] = array(
      array('data' => $box['bid'], 'class' => 'sitedoc_right'), 
      $info,
      array('data' => $box['format'], 'class' => 'sitedoc_right'),
      highlight_string($box['body'], TRUE),
      );
  } // End while.

  $howmany = count($rows);
  if ($howmany > 0) {
    $output .= theme('table', $header, $rows) ."\n";
    drupal_set_message(t('!count boxes found.', array('!count' => $howmany)), 'notice');
  }
  if ($orphans) {
    if ($warn) { drupal_set_message('Orphan boxes found.', 'warning'); }
  }
  return $output;
}

/**
 *   Produce the content types list.
 *
 *  Parameters:
 *    none
 *
 *  Return: HTML string
 **/
function sitedoc_content_types() {  
  $yesno = array(t('no'), t('yes'));
  $com_vals = array(t('disabled'), t('read-only'), t('read/write'));

  // The TRUE here forces the node module to refresh the list.
  $node_types = node_get_types('types', NULL, TRUE); 
  $header = array(t('Type'), 
                  array('data' => t('Count'), 'class' => 'sitedoc_right'),
                  t('Name') .'/<br>'. t('Module'),
                  t('Description'), 
                  t('Has title') .'/<br>'. t('Has body') .'/<br>'. t('Has help'),
                  array('data' => t('Min words'), 'class' => 'sitedoc_right'),
                  t('Custom') .'/<br>'. t('Modified') .'/<br>'. t('Locked'),
                  t('Workflow') .'/<br>'. t('Comments')
               );
  $num_contents = 0;
  $missing = FALSE;

  foreach ($node_types as $type => $info) {
    $num_contents ++;    
    $workflow = str_replace('status', 'published', implode(', ', variable_get('node_options_'. $info->type, array())));
    if (workflow == '') { $workflow = 'not published'; }
    $count = db_result(db_query('SELECT COUNT(n.nid) FROM {node} n WHERE n.type=\'%s\'', $info->type));
    if (module_exists($info->module)) { $mod = $info->module; }
    else {
      $mod = $info->module .' <span class="admin-missing">('. t('missing') .')</span>';
      $missing = TRUE;
     }
    $has_help = $info->help ? true : false;
    $rows[] = array(l($info->type, 'admin/content/types/'. $info->type),
                    array('data' => $count, 'class' => 'sitedoc_right'),
                    $info->name .'<br>'. $mod,
                    $info->description,
                    $yesno[$info->has_title] .'<br>'. $yesno[$info->has_body] .'<br>'. $yesno[$has_help], 
                    array('data' => $info->min_word_count, 'class' => 'sitedoc_right'),
                    $yesno[$info->custom] .'<br>'. $yesno[$info->modified] .'<br>'. $yesno[$info->locked],
                    $workflow .'<br>'. $com_vals[variable_get('comment_'. $info->type, 0)],
                ); 
   }  /* end foreach */

  if ($missing) { drupal_set_message($num_contents . t(' content types found. Missing modules identified.'), 'warning'); }
  else { drupal_set_message($num_contents . t(' content types found.'), 'notice'); }

  $output .= theme('table', $header, $rows);

  return $output ."\n";
}

/**
 *   Produce the vocabularies and terms list.
 *
 *  Parameters:
 *    none
 *
 *  Return: HTML string
 **/
function sitedoc_get_vocabularies() {
  if (!module_exists('taxonomy')) { return '<p>Taxonomy'. t(' module not enabled') .'</p>'; }
  $hier = array(t('Disabled'), t('Single'), t('Multiple'));
  $vocabularies = taxonomy_get_vocabularies();
  $rows = array();
  $header = array(t('Name (vid)'), t('Content Types'), t('Description'), t('Help'), t('Hierarchy'),
                  t('Terms'), t('Module'), t('Weight'),
             );
  foreach ($vocabularies as $vocabulary) {
    $types = array();
    foreach ($vocabulary->nodes as $type) {
      $types[] = $type;
     }  /* end foreach type */
    $t = array();
    if ($vocabulary->relations) { $t[] = t('related'); } 
    if ($vocabulary->multiple) { $t[] = t('multi-select'); } 
    if ($vocabulary->required) { $t[] = t('required'); } 
    if ($vocabulary->tags) { $t[] = t('free-tag'); } 
    $rows[] = array(
      'name' => check_plain($vocabulary->name) .' ('. $vocabulary->vid .')',
      'type' => implode(', ', $types),
      $vocabulary->description . _terms_list($vocabulary->vid),  /* subroutine gets terms */
      $vocabulary->help ? t('yes') : t('no'),
      $hier[$vocabulary->hierarchy],
      implode(', ', $t),
      $vocabulary->module,
      $vocabulary->weight,
      );
   }  /* end foreach vocab */
   
  $output .= theme('table', $header, $rows);
  return $output ."\n";
}

// Helper function to get vocabulary term info.
function _terms_list($vid) {
  $items = array();
  $thead = array(
    t('Term (tid)'), 
    array('data' => t('Count'), 'class' => 'sitedoc_right'),
    t('Description'),
    );
  $terms = taxonomy_get_tree($vid);

  foreach ( $terms as $term ) {
    // This query should be very fast and includes unpublished nodes.
    $count = db_result(db_query('SELECT COUNT(nid) FROM {term_node} WHERE tid=%d', $term->tid));

    // don't show terms with 0 count
    if ($count) {   
       $items[] = array(
         $term->name .' ('. $term->tid .')',
         array('data' => $count, 'class' => 'sitedoc_right'),
         $term->description);
         }
   } /* end foreach */

  return "\n". theme('table', $thead, $items);
}

/**
 *  Produce a table of taxonomy term usage by content type.
 *  This function is available only as a callable function;
 *   it is not called within this module.
 *
 *  Parameters:
 *    vid - the vocabulary id to show.
 *
 *  Return: HTML string
 **/
function sitedoc_term_count_by_type($vid) {
  $vocab = taxonomy_get_vocabulary($vid);
  if (!$vocab->name) {
    drupal_set_message(t('Vocabulary not found.'), 'warning');
    return t('Vocabulary not found.');
  }

  $output = '<h2>Usage of "'. $vocab->name .'" Category by Term and Content Type</h2>';
  if ($vocab->description) { $output .= '<h6>'. $vocab->description .'</h6>'; }

  $header = array(t('Term'));
  $rows = array();
  $type = array();

  // Build header row with types.
  foreach ($vocab->nodes as $key => $value) {
    $type[] = $value;
    $name = $value;
    $header[] =  array('data' => $name, 'align' => 'right') ;
  } /* end foreach */

  $terms = taxonomy_get_tree($vid);
  foreach ( $terms as $term ) {
    $line = array();
  if (module_exists('taxonomy_image')) {
    $img = taxonomy_image_display($term->tid, 'align="left" hspace="3"');
  }
  else { $img = NULL; }
    $line[] = $img . l($term->name, 'taxonomy/term/'. $term->tid) ."<br/><small>". $term->description ."</small>";
    foreach ($type as $content_type) {
      $count = sitedoc_term_count_nodes($term->tid, $content_type);
      if ($count) { /* don't show terms with 0 count */
        $line[] = array('data' => $count, 'align' => 'right');
      } /* end if count */
      else { $line[] = ' '; };
    } /* end each content_type */
    $rows[] = $line;
  } /* end foreach term */

  $output .= theme('table', $header, $rows);
  return $output;
}

/**
 *  Check if there are any term nodes entries that don't belong to nodes.
 *
 *  Parameters:
 *    Delete - TRUE | FALSE - whether or not to delete the orphans.
 *
 *  Return: HTML string
 **/
function sitedoc_check_orphan_term_node($delete_orphans=FALSE) {
  if (!module_exists('taxonomy')) { return '<p>Taxonomy'. t(' module not enabled') .'</p>'; }
  
  $tn = db_num_rows(db_query('SELECT DISTINCT(tn.nid) FROM {term_node} tn'));
  $n =  db_num_rows(db_query('SELECT DISTINCT(n.nid) FROM {node} n'));
  
  if ($tn > $n) {drupal_set_message(t('There are ') . $tn . t('term_nodes but only ') . $n . t(' nodes.'), 'warning');}
  else { return '<p>'. t('No orphan term_nodes found.') .'</p>'; }

  $header = array('nid', 'tid', 'term', 'vocab');
  $rows = array();
  $del = NULL;

  $result = db_query('SELECT tn.nid, tn.tid FROM {term_node} tn LEFT JOIN {node} n ON tn.nid=n.nid WHERE ISNULL(n.title) ORDER BY tn.nid, tn.tid');

  while ($tn = db_fetch_array($result)) {
     $term = taxonomy_get_term($tn['tid']);
     $vocab = taxonomy_get_vocabulary($tn['tid']);
     $delsql .= 'DELETE FROM term_node WHERE term_node.nid='. $tn['nid'] .' AND term_node.tid='. $tn['tid'] .' LIMIT 1; ';
     if ($delete_orphans) {
       db_query($delsql);
       drupal_set_message(t('Deleted term_node ') .'nid='. $tn['nid'] .', tid='. $tn['tid'], 'notice');
      }
     else { $del .= $delsql; }
     $rows[] = array($tn['nid'], $tn['tid'], $term->name, $vocab->name, $del);
   } /* end while */
   
  $output = theme('table', $header, $rows);
  if (!$delete_orphans) { $output .= $del; }
  
  return $output ."\n";
}

/**
 * Produce the sequences table info.
 *
 *  Parameters:
 *    none
 *
 *  Return: HTML string
 **/
function sitedoc_sequences() {
  $result = db_query("SELECT s.name, s.id FROM {sequences} s ORDER BY s.name");
  $header = array(t('Name'),
                  array('data' => t('Last ID'), 'class' => 'sitedoc_right'),
              );
  $rows = array();

  while ($seq = db_fetch_array($result)) {
    $rows[] = array($seq['name'],
                    array('data' => $seq['id'], 'class' => 'sitedoc_right'),
                );
   } /* end while */

  $output .= theme('table', $header, $rows);
  return $output ."\n";
}

/**
 *   Produce the node summary.
 *
 *  Parameters:
 *    TRUE - include comments count.
 *    FALSE - don't include comments count.
 *
 *  Return: HTML string
 **/
function sitedoc_node_summary($comment=FALSE, $show_size=99999, $max_size=99999) {
  $sql = "SELECT n.nid, n.title, n.type, n.status, n.promote, n.moderate, n.sticky, length(body) AS node_len";
  if ($comment) { $sql .= ", c.comment_count"; } 
  $sql .= " FROM {node} n LEFT JOIN {node_revisions} nr ON nr.vid=n.vid";
  if ($comment) { $sql .= " LEFT JOIN {node_comment_statistics} c ON c.nid=n.nid"; } 
  $result = db_query($sql);
  $counters = array();
  $types = array();
  $weight_detected = FALSE;
  drupal_set_message(db_num_rows($result) . t(' nodes found.'), 'notice');
  
  $toobig = 0;
  $biggies = array();

  while ($node = db_fetch_object($result)) {
    $node_kb = $node->node_len / 1024;
    if ($node_kb >= $show_size) {
      $biggies[] = array(l($node->title, 'node/'. $node->nid .'/edit'),
                      array('data' => number_format($node_kb, 2), 'align' => 'right'),
                  );
     } /* end if show size */
    if ($node_kb >= $max_size) { $toobig ++; }

    if (!in_array($node->type, $types)) { $types[] = $node->type; }
    $counters[$node->type]['type'] ++;
    if ($node->status) { $counters[$node->type]['published'] ++; }
    if ($node->promote) { $counters[$node->type]['promoted'] ++; }
    if ($node->moderate) { $counters[$node->type]['moderated'] ++; }
    if ($comment) { $counters[$node->type]['comments'] += $node->comment_count; }
     /* check if the weight module algorithm is in use */
    if ($node->sticky > 1 || $node->sticky < 0) { 
       _decode_sticky($node);
       if (!$weight_detected) {
          $weight_detected = TRUE;
          drupal_set_message(t('Weight-encoded in sticky field has been detected.'), 'warning');
        }
     }
    else { $node->weight = 0; }  /* set unweighted if not */
    if ($node->sticky) { $counters[$node->type]['sticky'] ++; }
    if ($node->weight <> 0) { $counters[$node->type]['weight'] ++; }
   } /* end while fetch */
  ksort($types);
  $header = array(t('Type'),
                  array('data' => t('Count'), 'class' => 'sitedoc_right'),
                  array('data' => t('Published'), 'class' => 'sitedoc_right'),
                  array('data' => t('Promoted'), 'class' => 'sitedoc_right'),
                  array('data' => t('Sticky'), 'class' => 'sitedoc_right'), 
                  array('data' => t('Weighted'), 'class' => 'sitedoc_right'),
                  array('data' => t('Moderated'), 'class' => 'sitedoc_right'),
                  $comment ? array('data' => t('Comments'), 'class' => 'sitedoc_right') : NULL,
             );
  $rows = array();
  $tot = 0; $pub = 0; $pro = 0; $sti = 0; $mod = 0;
  foreach ($types as $type) {
    $rows[] = array($type,
                    array('data' => $counters[$type]['type'], 'class' => 'sitedoc_right'),
                    array('data' => $counters[$type]['published'], 'class' => 'sitedoc_right'),
                    array('data' => $counters[$type]['promoted'], 'class' => 'sitedoc_right'),
                    array('data' => $counters[$type]['sticky'], 'class' => 'sitedoc_right'),
                    array('data' => $counters[$type]['weight'], 'class' => 'sitedoc_right'),
                    array('data' => $counters[$type]['moderated'], 'class' => 'sitedoc_right'),
                    $comment ? array('data' => $counters[$type]['comments'], 'class' => 'sitedoc_right') : NULL,
              );
    $tot += $counters[$type]['type'];
    $pub += $counters[$type]['published'];
    $pro += $counters[$type]['promoted'];
    $sti += $counters[$type]['sticky'];
    $wei += $counters[$type]['weight'];
    $mod += $counters[$type]['moderated'];
    $com += $counters[$type]['comments'];
   } /* end foreach type */
  $rows[] = array('',
                  array('data' => '------', 'class' => 'sitedoc_right'),
                  array('data' => '------', 'class' => 'sitedoc_right'),
                  array('data' => '------', 'class' => 'sitedoc_right'),
                  array('data' => '------', 'class' => 'sitedoc_right'),
                  array('data' => '------', 'class' => 'sitedoc_right'),
                  array('data' => '------', 'class' => 'sitedoc_right'),
                  $comment ? array('data' => '------', 'class' => 'sitedoc_right') : NULL,
              );
  $rows[] = array('<i>total</i>',
                  array('data' => $tot, 'class' => 'sitedoc_right'),
                  array('data' => $pub, 'class' => 'sitedoc_right'),
                  array('data' => $pro, 'class' => 'sitedoc_right'),
                  array('data' => $sti, 'class' => 'sitedoc_right'),
                  array('data' => $wei, 'class' => 'sitedoc_right'),
                  array('data' => $mod, 'class' => 'sitedoc_right'),
                  $comment ? array('data' => $com, 'class' => 'sitedoc_right') : NULL,
              );
  $output .= theme('table', $header, $rows);

  if ($toobig) {
    $output .= '<p><img src="/misc/watchdog-warning.png" width="17" height="17"> '
               . $toobig . t(' nodes exceed ') . $max_size .'KB.</p>';
   } /* end if toobig */
  else {
    $output .= '<p><img src="/misc/watchdog-ok.png" width="17" height="17"> '
               . t('No nodes exceed ') . $max_size .'KB.</p>';
   } /* end else */
  if (count($biggies)) {  
    $big_head = array(t('Title'),
                    t('Length (KB)'),
                );
    $fieldset = array(
      '#title' => t('Large Nodes'),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#value' => theme('table', $big_head, $biggies),
      );
    $output .=  theme('fieldset', $fieldset);
   } /* end biggie check */

  return $output ."\n";
}

// Helper module in case weight module algorithm is used.
function _decode_sticky(&$node) {
  $sticky = $node->sticky;     /* save value */
  $node->sticky = ($sticky > 0) ? 1 : 0;
  $node->weight = ($sticky > 0) ? 100 - $sticky : -100 - $sticky;
  return;
}

/**
 *   Produce the node access summary.
 *
 *  Parameters:
 *    None
 *
 *  Return: HTML string.
 **/
function sitedoc_node_access() {
//  $output = '<h3>'. t('Node Access Summary') ."</h3>\n";
  
  // How many nodes are not represented in the node_access table.
  $result = db_fetch_object(db_query('SELECT COUNT(n.nid) as num_nodes FROM {node} n LEFT JOIN {node_access} na ON n.nid = na.nid WHERE na.nid IS NULL'));
  if ($num = $result->num_nodes) {
    $output .= '<p><img src="/misc/watchdog-warning.png" width="17" height="17">'.
               t('You have !num nodes in your node table which are not represented in your node_access table.  If you have an access control module installed, these nodes may be hidden from all users.  This could be caused by publishing nodes before enabling the access control module.  If this is the case, manually updating each node should add it to the node_access table and fix the problem. Click here to !rebuild.', array('!num' => l($num, 'admin/build/sitedoc_node_access_view/'), '!rebuild' => l(t('rebuild permissions'), 'admin/content/node-settings/rebuild'))) ."</p>\n";
  }
  else {
    $output .= '<p><img src="/misc/watchdog-ok.png" width="17" height="17">'
               . t('All nodes are represented in the node_access table.') ."</p>\n";
   } /* end else */

  // Warn user if they have any entries that could grant access to all nodes.
  $rows = array();
  $result = db_query('SELECT DISTINCT na.realm FROM {node_access} na WHERE na.nid=0 AND na.gid=0');
  if (db_num_rows($result)) {
    while ($row = db_fetch_object($result)) { $rows[] = $row->realm; }
    $output .= '<h5>'. t('Access Granted to All Nodes (All Users)') ."</h5>\n";
    $output .= '<p>'. t('Your node_access table contains entries (!r) that may be granting all users access to all nodes.  Depending on which access control module(s) you use, you may want to delete these entries.  If you are not using an access control module, you should probably leave these entries as is.', array('!r' => implode(',  ', $rows))) ."</p>\n";
    }

 // A similar warning to the one above, but slightly more specific.
  $result = db_query('SELECT DISTINCT na.realm FROM {node_access} na WHERE na.nid=0 AND na.gid!=0');
  if (db_num_rows($result)) {
    $output .= '<h5>'. t('Access Granted to All Nodes (Some Users)') ."</h5>\n";
    $output .= '<p>'. t('Your node_access table contains entries that may be granting some users access to all nodes.  This may be normal, depending on which access control module(s) you use.') ."</p>\n";
    $headers = array(t('realm'));
    $rows = array();
    while ($row = db_fetch_object($result)) {
      $rows[] = array($row->realm);
     } /* end while */
    $output .= theme('table', $header, $rows);
   } /* end all nodes */

    $headers = array(t('Realm'),
                     t('Nodes'),
                     t('Notes'),
                 );
    $rows = array();

 // Count nodes by realm.
  $result = db_query('SELECT DISTINCT na.realm, COUNT(DISTINCT na.nid) as node_count FROM {node_access} na GROUP BY na.realm');
  $all = FALSE;

  if (db_num_rows($result)) {
    while ($row = db_fetch_object($result)) {
      if ($row->realm == 'all') {
        $all = TRUE;
        $realm_type = t('Public nodes');
        $caption = t('This realm grants all users access to some specific nodes. If some of your content is available to the public, this may be normal.');
       }
      else {
        $realm_type = t('Private nodes');
        $caption = t('This realm grants limited access to some specific nodes.');
       }
      $rows[] = array($row->realm, l($row->node_count, 'admin/build/sitedoc_node_access_view/'. $row->realm) .' '. $realm_type, $caption);
     } /* end while */
   if ($all && count($rows) > 1) { $output .= theme('table', $header, $rows); }
  }

  return $output ."\n";
}

/** 
 *  Menu call back to produce a list of nodes within a given access realm.
 *
 *   parameters:
 *     realm name
 *
 *   returns:
 *     HTML string
 **/
function sitedoc_node_access_view($realm=NULL) {
  $rows = array();
  $header = array(t('Type'),
                  t('Title') .' &nbsp;&nbsp;&nbsp;<small> '. t('click to edit') .'</small>',
              );

  $sql = 'SELECT DISTINCT(n.nid), n.type, n.title FROM {node} n LEFT JOIN {node_access} na ON n.nid = na.nid WHERE na.realm ';
  $count = 'SELECT COUNT(DISTINCT(n.nid)) FROM {node} n LEFT JOIN {node_access} na ON n.nid = na.nid WHERE na.realm ';
  if (is_null($realm)) {
    $output .= '<h3>'. t('Unprotected Nodes') .'</h3>';
    $sql .= 'IS NULL';
    $count .= 'IS NULL';
   }
  else { 
    $output .= '<h3>'. t('Nodes in Realm: ') . ucfirst($realm) .'</h3>';
    $sql .= "= '". $realm ."'";
    $count .= "= '". $realm ."'";
   }

  $result = pager_query($sql, 50, 0, $count);
  while ($node = db_fetch_object($result)) {
    $rows[] = array($node->type,
                    l($node->title, 'node/'. $node->nid .'/edit'),
                );
   } /* end while node */
  $output .= theme('table', $header, $rows);
  $output .= theme('pager', array(), 50);

  return $output ."\n";
}

/**
 *  Produce the user roles list.
 *
 *  Parameters:
 *    1) TRUE - permissions listed as unordered list
 *       FALSE - permissions listed as stream (default)
 *    2) TRUE - include users in roles (default)
 *       FALSE - do not include users 
 *
 *  Return: HTML string
 **/
function sitedoc_get_roles($list = FALSE, $users = FALSE) {
  $sql = 'SELECT r.rid, r.name, p.perm, p.tid FROM {role} r INNER JOIN {permission} p ON p.rid = r.rid';
  $result = db_query($sql);
  $output = NULL;
  $header = array(array('data' => 'Rid', 'class' => 'sitedoc_right'),
                  t('Name'),
                  t('Permissions'),
                  t('Blocks'),
                  $users ? t('Users') : NULL,
                  array('data' => 'Tid', 'class' => 'sitedoc_right'),
              );

  while ($role = db_fetch_object($result)) {
    // Get which blocks they can see.
    $bresult = db_query('SELECT b.module, b.delta FROM {blocks_roles} b WHERE b.rid = %d', $role->rid);
    $blk_list = array();
    while ($block = db_fetch_object($bresult)) {
      $blk_list[] = $block->module .'/'. $block->delta;
     } /* end while block */

    // Get which users have this role.
    $uresult = db_query('SELECT u.name, u.uid FROM {users_roles} r INNER JOIN {users} u ON u.uid = r.uid WHERE r.rid = %d', $role->rid);
    $user_list = array();
    while ($user = db_fetch_object($uresult)) {
    // Make the user a link.
      $user_list[] = l($user->name .'('. $user->uid .')', 'user/'. $user->uid);
     } 

    $rows[] = array(array('data' => $role->rid, 'class' => 'sitedoc_right'),
                     $role->name,
                     $list ? theme('item_list', explode(', ', $role->perm)) : $role->perm,
                     $list ? theme('item_list', $blk_list) : implode(', ', $blk_list),
                     $users ? ($list ? theme('item_list', $user_list) : implode(', ', $user_list)) : NULL,
                     array('data' => $role->tid, 'class' => 'sitedoc_right'),
                 );
   } /* end while role */
  
  $howmany = count($rows);
  if ($howmany > 0) {
    drupal_set_message(t('!count Roles found.', array('!count' => $howmany)), 'notice');
//    $output = '<h3>'. t('Roles and Permissions') .'</h3>';
    $output = theme('table', $header, $rows);
  }
  return $output ."\n";
}

/**
 *  Produce the system variables list.
 *
 *  Parameters:
 *    None
 *
 *  Return: HTML string
 **/
function sitedoc_get_variables() {
  // $conf is the cached array of system variables 
  global $conf;   

  // Save it so we can sort it
  $vars = $conf;  
  ksort($vars);

//  $output = '<h3>'. t('System Variables') .'</h3>';
  drupal_set_message(count($vars) . t(' system variables found.'), 'notice');

  $header = array(t('Name'), t('Value'));
  $rows = array();

  foreach ($vars as $name => $value) {
    if (is_array($vars[$name])) { $value = _handle_array($vars[$name]); }  /* use helper function to do an array */
    else { $value = check_plain($vars[$name]); }

    $rows[] = array($name, $value);
   } /* end foreach */

  $output .= theme('table', $header, $rows);
  return $output ."\n";
}

// Helper function for traversing an array.
// Since some arrays may contain arrays, this function can be called recursively.
// Also handles objects.
function _handle_array(&$array) {
  $header = array(t('Name'), t('Value'));
  $rows = array();

  if (count($array) == 0) { return t('Empty'); }

  foreach ($array as $name => $value) {
    if (is_array($array[$name])) { $value = _handle_array($array[$name]); }  /* recursive call for imbedded array */
    else { 
      if (is_string($array[$name]) || is_integer($array[$name]) || is_null($array[$name])) { $value = check_plain($array[$name]); }
      else {
        if (is_bool($array[$name])) { $value = $array[$name] ? t('True') : t('False'); }
        else { 
          if (is_object($array[$name])) {
            $obj = array();
            foreach ($array[$name] as $key => $val) {
              if (is_array($val)) { $val = _handle_array($val); }
               $obj[] = "[$key] => $val";
             } /* end foreach obj */
            $value = theme('item_list', $obj);
           } /* end if obj */
          else {
            $value = '<strong>'. gettype($array[$name]) .'</strong>';
            drupal_set_message('Tell Nancy I found a '. gettype($array[$name]) .' in the variables.', 'warning');
           } /* end else obj */
        } /* end else bool */
       } /* end else string */
     } /* end else array */
    $rows[] = array($name, $value);
   } /* end foreach */

  return theme('table', $header, $rows);
}

/**
 * Produce the site-wide contacts (email) list.
 *
 *  Parameters:
 *    None
 *
 *  Return: HTML string
 **/
function sitedoc_get_contacts() {
  $output = NULL;
  if (!module_exists('contact')) { return '<p>Contact'. t(' module not enabled') .'</p>'; }
  $sql = 'SELECT * FROM {contact} c ORDER BY c.selected DESC, c.cid ASC';
  $result = db_query($sql);
  $header = array(array('data' => 'Cid', 'class' => 'sitedoc_right'),
                  t('Category'), 
                  t('Recipients'), 
                  array('data' => t('Weight'), 'class' => 'sitedoc_right'),
                  array('data' => t('Selected'), 'align' => 'center'),
               );
  $rows = array();
  while ($contact = db_fetch_object($result)) {
    $recips = explode(',', $contact->recipients);
    if (count($recips) > 1) { $recip_list = theme('item_list', $recips); }
    else { $recip_list = $contact->recipients; }
    $rows[] = array(array('data' => $contact->cid, 'class' => 'sitedoc_right'),
                    $contact->category,
                    $recip_list,
                    array('data' => $contact->weight, 'class' => 'sitedoc_right'),
                    array('data' => $contact->selected ? 'yes' : 'no', 'align' => 'center'),
                 );
   } /* end while role */
  
  $howmany = count($rows);
  if ($howmany > 0) {
    drupal_set_message(t('!count Contacts found.', array('!count' => $howmany)), 'notice');
//    $output = '<h3>'. t('Contacts') .'</h3>';
    $output = theme('table', $header, $rows);
  }
  return $output ."\n";
}

/**
 * Produce the user profile fields list.
 *
 *  Parameters:
 *    string - limits the list to only this category
 *
 *  Return: HTML string
 **/
function sitedoc_profile_fields() {
  if (!module_exists('profile')) { return '<p>Profile '. t('module not enabled') .'</p>'; }
  $output = NULL;

  // Allow an optional param to limit the display to a specific category.
  if (func_num_args() > 0) { $category = func_get_arg(0); }

//  $output = '<h3>'. t('Profile Fields') .'</h3>';
  $no_yes = array(t('No'), t('Yes')); 
  $viz = array(t('Hidden'), t('Private'), t('Profile'), t('Public'));
  // This is to save display space.
  $short_type = array('textfield' => t('Text'),
                      'textarea' => t('Area'),
                      'URL' => t('URL'),
                      'checkbox' => t('Check'),
                      'selection' => t('select'),
                      'list' => t('Free'),
                      'date' => t('Date'),
                  );

  $header = array(array('data' => t('Fid'), 'class' => 'sitedoc_right'),
                  t('Title'),
                  t('Name'),
                  t('Explanation'),
                  t('Page'),
                  t('Type'),
                  array('data' => t('Weight'), 'class' => 'sitedoc_right'),
                  array('data' => t('Visiblity'), 'align' => 'center'),
                  array('data' => t('Required'), 'align' => 'center'),
                  array('data' => t('Register'), 'align' => 'center'),
                  array('data' => t('Auto complete'), 'align' => 'center'),
                  t('Options'),
             ); 
  if ($category) { $where = " WHERE p.category='$category'"; }
  else { $where = ' '; }
  $result = db_query('SELECT * FROM {profile_fields} p'. $where .' ORDER BY p.category, p.weight, p.name');
  $save_cat = '';

  while ($field = db_fetch_object($result)) {
    if ($field->category <> $save_cat) {
      if (!empty($save_cat)) { /* flush previous rows, if any */
        $output .= theme('table', $header, $rows);
       } /* end flush rows */
      $output .= '<h4>'. t('Category: ') . $field->category .'</h4>';
      $save_cat = $field->category;
      $rows = array();
     } /* end cat change */
    $rows[] = array(array('data' => $field->fid, 'class' => 'sitedoc_right'),
                    $field->title,
                    $field->name,
                    $field->explanation,
                    $field->page,
                    $short_type[$field->type],
                    array('data' => $field->weight, 'class' => 'sitedoc_right'), 
                    array('data' => $viz[$field->visibility], 'align' => 'center'), 
                    array('data' => $no_yes[$field->required], 'align' => 'center'),
                    array('data' => $no_yes[$field->register], 'align' => 'center'),
                    array('data' => $no_yes[$field->autocomplete], 'align' => 'center'),
                    $field->options,
                );
   } /* end while field */
  
  $howmany = count($rows);
  drupal_set_message(t('!count Profile fields found.', array('!count' => $howmany)), 'notice');
  if ($howmany > 0) { $output .= theme('table', $header, $rows) ."\n"; }
  return $output;
}

/**
 * Produce the URL Alias list and checks for orphans.
 *
 *  Parameters:
 *    string - limits the list to only this category
 *
 *  Return: HTML string
 **/
function sitedoc_url_alias() {
  if (!module_exists('path')) { return '<p>Path '. t('module not enabled') .'</p>'; }
  $output = NULL;
//  $output = '<h3>'. t('URL Aliases') .'</h3>';
  $rows = array();
  $last_src = NULL;
  $problems = 0;
  // For reverse checking.
  $node_alias = array();

  $header = array(array('data' => t('pid'), 'class' => 'sitedoc_right'),
                  t('Source'),
                  t('Destination'),
                  t('Notes'),
                  t('Operation'),
             ); 
  $result = db_query('SELECT u.pid, u.src, u.dst FROM {url_alias} u ORDER BY u.src, u.dst');
//  $alias_count = db_num_rows($result);

  while ($path = db_fetch_object($result)) {
    $notes = array();
    $ops = array();
    if ($last_src == $path->src) { $notes[] = t('duplicate source'); } 
    else { $last_src = $path->src; }
  
    // Ignore "special" paths.
    if (substr($path->src, 0, 1) != '<') { 
      $p = explode('/', $path->src);
      $edit = 'edit';
    // So pseudo HTML shows (like <front>)
      $source = check_plain($path->src);   
 
      // Check first part of path.
      switch ($p[0]) {  

        case 'node':
      // Missing nid?
          if (empty($p[1])) { 
            $notes[] = '&lt;front&gt;?';
            break;
           } /* end if empty */
          $node_exists = db_num_rows(db_query('SELECT n.nid FROM {node} n WHERE n.nid = %d', $p[1]));
          if ($node_exists) { 
            $source = $path->src;
            $ops[] = l(t('edit node'), $path->src .'/edit');
           }
          else {
            $notes[] = '<span class="admin-missing">'. t('missing node') .'</span>'; 
            $edit = 'delete';
           }
          $node_alias[$p[1]] = TRUE;
          break;

        case 'taxonomy':
          $taxo_exists = db_num_rows(db_query('SELECT * FROM {term_data} WHERE tid = %d', $p[2]));
          if (!$taxo_exists) { $notes[] = '<span class="admin-missing">'. t('missing taxonomy term') .'</span>'; }
          break;

        case 'faq':   /* same as taxo/term */
          $taxo_exists = db_num_rows(db_query('SELECT * FROM {term_data} WHERE tid = %d', $p[1]));
          if (!$taxo_exists) { $notes[] = '<span class="admin-missing">'. t('missing faq taxonomy term') .'</span>'; }
          break;

        case 'user': 
          $node_exists = db_num_rows(db_query('SELECT u.uid FROM {users} u WHERE u.uid = %d', $p[1]));
          if (!$node_exists) { $notes[] = '<span class="admin-missing">'. t('missing user') .'</span>'; }
          break;

        case 'contact':  /* nothing to do */
          break;

        default:
          if (module_exists($p[0])) { $notes[] = '<span class="admin-enabled">'. t('ask the !name module', array('!name' => $p[0])) .'</span>'; }
          else { $notes[] = '<span class="admin-missing">'. t('unhandled path type') .'</span>'; }

       } /* end switch $p(0) */
     } /* end if not < */
    
    $problems += count($notes);
    $ops[] = l($edit .' '. t('path'), 'admin/build/path/'. $edit .'/'. $path->pid);
    $rows[] = array(array('data' => $path->pid, 'class' => 'sitedoc_right'),
                    $source,
                    $path->dst,
                    implode(', ', $notes),
                    implode(', ', $ops),
                );
   } /* end while path */
  
  $howmany = count($rows);
  if ($howmany > 0) {
    $output .= theme('table', $header, $rows);
  }
  
  //  Now run through the nodes to see if they have an alias.
  $rows = array();

  $result = db_query('SELECT n.nid, n.title FROM {node} n ORDER BY n.nid');
  while ($node = db_fetch_object($result)) {
    if (!isset($node_alias[$node->nid])) { /* does node NOT have an alias? */
      $rows[] = array($node->title, l(t('edit'), 'node/'. $node->nid .'/edit'));
     } /* end isset */
   } /* end while node */

  if (count($rows) > 0) {
//    $output .= '<h4>'. t('Nodes without URL Alias') .'</h4>';
//    $output .= theme('table', array(t('Title'), t('Operation')), $rows);
    $fieldset = array(
      '#title' => t('Nodes without URL Alias'),
      '#collapsible' => TRUE,
      '#collapsed' => count($rows) > 10,
      '#value' => theme('table', array(t('Title'), t('Operation')), $rows),
      );
    $output .=  '<br/>'. theme('fieldset', $fieldset);
  }

  $msg = t('!count URL Aliases found', array('!count' => $howmany));
  if ($problems) { $msg .= ' '. t('with !problems notes.', array('!problems' => $problems)) .' '; }
  else { $msg .= '.'; }
  if (count($rows)) { $msg .= t('!count nodes without aliases found.', array('!count' => count($rows))); }
  drupal_set_message($msg, 'notice');

  return $output ."\n";
}

function sitedoc_term_count_nodes($tid = 0, $type = NULL) {
  if ($type) {
    $count = db_result(db_query(db_rewrite_sql("SELECT COUNT(n.nid) FROM {term_node} t INNER JOIN {node} n ON t.nid = n.nid WHERE t.tid = %d AND n.status = 1 AND n.type = '%s'"), $tid, $type));
  }
  else {
    $count = db_result(db_query(db_rewrite_sql('SELECT COUNT(n.nid) FROM {term_node} t INNER JOIN {node} n ON t.nid = n.nid WHERE t.tid = %d AND n.status = 1'), $tid));
  }
  return $count;
}

/*
 *  This function gets the Input Format and Filter information.
 */
function sitedoc_filters() {
  $output = "\n<h2>". t('Filters and Input Formats') .'</h2>';
  $filter_list = array();

  // Find out which modules provide filters.
  $filter_modules = module_implements('filter', TRUE);
  foreach ($filter_modules as $module) {
    $list = module_invoke($module, 'filter', 'list');
    foreach ($list as $delta => $name) {
      $filter_list[$module][$delta] = array(
        'name' => $name,
        'used' => array(),
        );
    }
  }

  $output .= "\n<h4>". t('Input Formats') ."</h4>\n";

  $rows = array();
  $filter_names = array();
  $count = 0;
  $output .= "\n<table>\n<tr>\n";
  $output .= "\n<th>". t('Format') .'</th>'
    .'<th>'. t('Name') .'</th>'
    .'<th>'. t('Roles') .'</th>'
    .'<th>'. t('Cache') .'</th>'
    .'<th>'. t('Filters') .'</th>'
    ."\n</tr>\n";

  // Get Input Formats.
  $result = db_query("SELECT * FROM {filter_formats}");
  while ($input = db_fetch_array($result)) {
    ++$count;
    $output .= "\n".'<tr class="'. ($count % 2 ? 'odd' : 'even') .'">';  

    $name = $input['name'];
    $format = $input['format'];
    $filter_names[$format] = $name;

    // Get the list of roles for this filter.
    // The roles list always starts and ends with a comma.
    $roles = substr($input['roles'], 1, strlen($input['roles']) - 2);
    $role_names = array();
    $r_result = db_query("SELECT name FROM {role} WHERE rid IN (%s)", $roles);
    while ($r = db_fetch_array($r_result)) { $role_names[] = $r['name']; }
    $roles = theme('item_list', $role_names);

    // Get the filter info.
    $f_rows = array();
    $filter_stuff = "\n<table><tr><th>". t('Name (Module, Delta)') .'</th><th>'.
      t('No Cache') .'</th><th>'.
      t('Weight') .'</th></tr>';
    $f_result = db_query("SELECT * FROM {filters} WHERE format=%d ORDER BY format, weight", $format);
    while ($filter = db_fetch_array($f_result)) {
      $module = $filter['module'];
      $delta = $filter['delta'];
      $filter_stuff .= "\n".'<tr>'
        .'<td>'. ucwords($filter_list[$module][$delta]['name']) .' ('. ucwords($module) .', '. $delta .')</td>'
        .'<td align="center">'. (module_invoke($module, 'filter', 'no cache', $delta, $format) ? 'True' : null) .'</td>'
        .'<td align="center">'. $filter['weight'] .'</td>'
        .'<tr>';
      $filter_list[$module][$delta]['used'][] = $filter_names[$format];
    }
    $filter_stuff .= "</table>\n";

    $cache = $input['cache'] ? '<img src="/misc/watchdog-ok.png"' : '<img src="/misc/watchdog-warning.png"';

    $output .= '<td valign="top">'. $format .'</td>'
      .'<td valign="top">'. $name .'</td>'
      .'<td valign="top">'. $roles .'</td>'
      .'<td align="center" valign="top">'. $cache .'</td>'
      .'<td valign="top">'. $filter_stuff .'</td>'
      ."\n</tr>";  
  }
  $output .= "\n</table>\n";

  $output .= "\n<h4>". t('Available Filters') ."</h4>\n";

// Show usage info.
  $list_rows = array();
  foreach ($filter_list as $module => $deltas) {
    $descs = "\n<table>";
    foreach ($deltas as $delta => $values) {
      $name = ucwords($values['name']);
      $descs .= "\n".'<tr><td align="center">'. $delta .'</td><td width="100">'. $name .'</td><td>'.  $values['desc'] .'</td></tr>';
    }
    $descs .= '</table>';
    $used = $filter_list[$module][$delta]['used'];
    $list_rows[] = array(ucwords($module), $descs,
      count(used) ? implode(', ', $used) : t('None'),
      );
  }
  $output .= theme('table', array(t('Module'), t('Provides'), t('Used In')), $list_rows);

  return $output;
}