You are here

quotes.module in Quotes 5

Same filename and directory in other branches
  1. 6 quotes.module
  2. 7 quotes.module

File

quotes.module
View source
<?php

/**
 * @file
 * The quotes module allows users to maintain a list of quotes that
 * can be displayed in any number of administrator-defined quote
 * blocks.
 *
 * @copyright Copyright (c) 2003-2007 Jim Riggs.  All rights reserved.
 * @author Jim Riggs <drupal at jim and lissa dot com>
 */
if (module_exists('views')) {
  include "quotes_views.inc";
}
define('QUOTES_VERSION', '$Id$');
define('BIOS_PATH', 'admin/settings/quotes/bios');

//********************************************************************

//* Drupal Hooks

//********************************************************************/

/**
 * Implementation of hook_perm().
 */
function quotes_perm() {
  return array(
    'access quotes',
    'administer quotes',
    'create quotes',
    'import quotes',
    'edit own quotes',
    'promote quotes to block',
    'view my quotes',
  );
}

/**
 * Implementation of hook_access().
 */
function quotes_access($op, $node) {
  global $user;
  switch ($op) {
    case 'create':
      return user_access('create quotes') || user_access('import quotes') || user_access('edit own quotes');
    case 'update':
    case 'delete':
      if (user_access('edit own quotes') && $user->uid == $node->uid) {
        return TRUE;
      }
      if (user_access('administer quotes')) {
        return TRUE;
      }
      return;
    case 'view':
      if (user_access('access quotes')) {
        return TRUE;
      }
      return;
  }
}

/**
 * Implementation of hook_menu().
 */
function quotes_menu($may_cache) {
  global $user;
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'node/add/quotes',
      'title' => t('Quotes'),
      'access' => user_access('create quotes') || user_access('import quotes') || user_access('edit own quotes'),
    );
    $items[] = array(
      'path' => 'node/add/quotes/add',
      'title' => t('Add'),
      'access' => user_access('create quotes') || user_access('edit own quotes'),
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'node/add/quotes/import',
      'title' => t('Import'),
      'access' => user_access('import quotes'),
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'quotes',
      'title' => t('Quotes'),
      'access' => user_access('access content'),
      'callback' => '_quotes_page',
      'type' => MENU_SUGGESTED_ITEM,
    );
    $items[] = array(
      'path' => 'quotes/author',
      'title' => t('Quotes by author'),
      'access' => user_access('access content'),
      'callback' => 'quotes_author',
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'quotes/feed',
      'title' => t('RSS feed'),
      'access' => user_access('access content'),
      'callback' => '_quotes_feed_last',
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'quotes/autocomplete/author',
      'title' => 'Autocomplete Author Field',
      'access' => user_access('access quotes'),
      'callback' => '_quotes_autocomplete_author',
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'quotes/autocomplete/citation',
      'title' => 'Autocomplete Citation Field',
      'access' => user_access('access quotes'),
      'callback' => '_quotes_autocomplete_citation',
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/settings/quotes',
      'title' => t('Quotes'),
      'description' => t('Configure Quotes module options and blocks.'),
      'access' => user_access('administer quotes'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        '_quotes_admin_settings',
      ),
    );
    $items[] = array(
      'path' => 'admin/settings/quotes/general',
      'title' => t('General'),
      'access' => user_access('administer quotes'),
      'type' => MENU_DEFAULT_LOCAL_TASK,
      'weight' => -1,
    );
    $items[] = array(
      'path' => 'admin/settings/quotes/blocks',
      'title' => t('Configure blocks'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        '_quotes_blocks',
      ),
      'access' => user_access('administer quotes'),
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/settings/quotes/export',
      'title' => t('Export'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'quotes_export',
      ),
      'access' => user_access('administer quotes'),
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => BIOS_PATH,
      'title' => t('Bios'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'quotes_bios',
      ),
      'access' => user_access('administer quotes'),
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/settings/quotes/delete',
      'title' => t('Delete quote block'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        '_quotes_block_delete',
      ),
      'type' => MENU_CALLBACK,
    );
  }
  else {
    drupal_add_css(drupal_get_path('module', 'quotes') . '/quotes.css');

    // User Edit quotes
    if (arg(0) == 'user' && is_numeric(arg(1))) {
      include drupal_get_path('module', 'quotes') . '/quotes.user.inc';
      $items[] = array(
        'path' => 'user/' . arg(1) . '/quotes',
        'title' => 'My Quotes',
        'callback' => 'quotes_user_page',
        'callback arguments' => array(
          arg(1),
        ),
        'access' => user_access('view my quotes'),
        'type' => MENU_LOCAL_TASK,
        'weight' => 2,
      );
    }
    $items[] = array(
      'path' => "quotes/{$user->uid}",
      'title' => t('My quotes'),
      'access' => $user->uid && (user_access('create quotes') || user_access('import quotes') || user_access('edit own quotes')),
      'type' => variable_get('quotes_show_myquotes', TRUE) ? MENU_NORMAL_ITEM : MENU_CALLBACK,
    );
  }
  return $items;
}

/**
 * Implementation of hook_node_info().
 */
function quotes_node_info() {
  return array(
    'quotes' => array(
      'name' => t('Quotes'),
      'module' => 'quotes',
      'description' => t('A quote is a famous, infamous, humorous, witty, or otherwise noteworthy quotation or fortune file entry. Users can maintain personal lists of quotations and display quotes in one or more blocks. Quotes can be entered one at a time or mass imported in either tab-separated text or fortune file format.'),
    ),
  );
}

/**
 * Function to provide autocomplete for author field.
 *
 * @param $string - the string currently entered by the user.
 * @return array of matches in JSON format.
 */
function _quotes_autocomplete_author($string) {
  $matches = array();
  $result = db_query("SELECT name FROM {quotes_authors} WHERE LOWER(name) LIKE LOWER('%%%s%%')", $string);
  while ($row = db_fetch_object($result)) {
    $matches[$row->name] = check_plain($row->name);
  }
  print drupal_to_js($matches);
}

/**
 * Function to provide autocomplete for citation field.
 *
 * @param $string - the string currently entered by the user.
 * @return array of matches in JSON format.
 */
function _quotes_autocomplete_citation($string) {
  $matches = array();
  $result = db_query("SELECT citation FROM {quotes} WHERE LOWER(citation) LIKE LOWER('%%%s%%')", $string);
  while ($row = db_fetch_object($result)) {
    $matches[$row->citation] = check_plain($row->citation);
  }
  print drupal_to_js($matches);
}

/**
 * Implementation of hook_form().
 */
function quotes_form(&$node, &$param) {
  global $_quotes_importing;
  $form = array(
    'quotes_data' => array(),
  );
  if ($_quotes_importing || arg(3) != 'import') {
    $form['title'] = array(
      '#type' => 'textfield',
      '#title' => t('Title'),
      '#required' => FALSE,
      '#default_value' => $node->title,
      '#description' => t('Enter the title for this quote. If you include the variable %id, it will be replaced by the newly created quote\'s node ID.', array(
        '%id' => '%id',
      )),
      '#weight' => -10,
    );
    $form['quotes_data']['body'] = array(
      '#type' => 'textarea',
      '#title' => t('Quote'),
      '#required' => TRUE,
      '#default_value' => $node->body,
    );
    $form['quotes_data']['quotes_author'] = array(
      '#type' => 'textfield',
      '#title' => t('Author'),
      '#autocomplete_path' => 'quotes/autocomplete/author',
      '#rows' => 1,
      '#maxlength' => 1023,
      '#default_value' => $node->quotes_author,
    );
    $form['quotes_data']['quotes_citation'] = array(
      '#type' => 'textfield',
      '#title' => t('Citation'),
      '#autocomplete_path' => 'quotes/autocomplete/citation',
      '#rows' => 1,
      '#default_value' => $node->quotes_citation,
    );
  }
  else {
    if (!user_access('import quotes')) {
      drupal_access_denied();
    }
    $form['title'] = array(
      '#type' => 'textfield',
      '#title' => t('Title'),
      '#required' => FALSE,
      '#default_value' => $node->title,
      '#description' => t('Enter the title that will be used for all imported quotes. If you include the variable %id, it will be replaced by the newly created quote\'s node ID.', array(
        '%id' => '%id',
      )),
      '#weight' => -10,
    );
    $form['quotes_data']['quotes_format'] = array(
      '#type' => 'radios',
      '#title' => t('Format'),
      '#required' => TRUE,
      '#default_value' => $node->quotes_format ? $node->quotes_format : 'text',
      '#options' => array(
        'text' => t('Tab-separated text'),
        'fortune' => t('Fortune file'),
      ),
    );
    $form['quotes_data']['body'] = array(
      '#type' => 'textarea',
      '#title' => t('Quotes'),
      '#required' => TRUE,
      '#rows' => 20,
      '#default_value' => $node->body,
    );
  }
  if (user_access('promote quotes to block')) {
    $form['quotes_data']['quotes_promote'] = array(
      '#type' => 'checkbox',
      '#title' => t('Display in quote blocks'),
      '#default_value' => isset($node->quotes_promote) ? $node->quotes_promote : 1,
    );
  }
  $form['quotes_data']['filter'] = filter_form($node->format);
  return $form;
}

/**
 * Implementation of hook_validate().
 */
function quotes_validate(&$node) {
  global $_quotes_importing;

  // bail if we are actively importing or doing a single quote
  if ($_quotes_importing || arg(3) != 'import') {
    return;
  }
  _quotes_parse_import($node, TRUE);
}

/**
 * Implementation of hook_submit().
 */
function quotes_submit(&$node) {
  global $_quotes_importing;

  // Bail if we are actively importing or doing a single quote.
  if ($_quotes_importing || arg(3) != 'import') {
    return;
  }

  // If the node is being submitted, we will perform the actual import
  // here, submitting each imported quote as a separate node.
  $_quotes_importing = TRUE;
  $count = 0;
  foreach (_quotes_parse_import($node, TRUE) as $quote) {
    ++$count;
    $temp = $node;
    $temp->body = $quote->body;
    $temp->teaser = '';
    $temp->quotes_author = $quote->quotes_author;
    $temp->quotes_citation = $quote->quotes_citation;
    drupal_execute('quotes_node_form', (array) $temp, array(
      'type' => 'quotes',
    ));
    if (form_get_errors()) {
      form_set_error('body', t('Only the first !count quotes were imported.', array(
        '!count' => $count,
      )));
      return;
    }
  }
  $_quotes_importing = FALSE;
  drupal_set_message(t('!count quotes imported.', array(
    '!count' => $count,
  )));
  drupal_goto('quotes');
}

/**
 * Implementation of hook_load().
 */
function quotes_load($node) {
  $obj = db_fetch_object(db_query("SELECT a.aid AS quotes_aid, a.name AS quotes_author, a.bio AS quotes_bio, q.citation AS quotes_citation, q.promote AS quotes_promote FROM {quotes} q JOIN {quotes_authors} a USING (aid) WHERE q.vid = %d", $node->vid));
  return $obj;
}

/**
 * Implementation of hook_insert().
 * This inserts the quote-specific information into the quotes
 * tables and also handles the %id variable in the node title.
 */
function quotes_insert($node) {
  $aid = _quotes_handle_author($node->quotes_author);
  db_query("INSERT INTO {quotes} (nid, vid, aid, citation, promote) VALUES (%d, %d, '%d', '%s', %d)", $node->nid, $node->vid, $aid, $node->quotes_citation, $node->quotes_promote);

  // replace %id variable in title
  if (strpos($node->title, '%id') !== FALSE) {
    $node->title = str_replace('%id', $node->nid, $node->title);
    db_query("UPDATE {node} SET title = '%s' WHERE vid = %d", $node->title, $node->vid);
    db_query("UPDATE {node_revisions} SET title = '%s' WHERE vid = %d", $node->title, $node->vid);
  }
}

/**
 *  Helper function to fetch or insert an author.
 *
 *  @param $author
 *    The text of the author's name.
 *
 *  @return
 *    The aid for the author in the quotes_authors table.
 */
function _quotes_handle_author($author) {
  $aid = db_result(db_query("SELECT qa.aid FROM {quotes_authors} qa WHERE qa.name='%s'", $author));
  if (empty($aid)) {
    $result = db_query("INSERT INTO {quotes_authors} (name) VALUES ('%s')", $author);
    if ($result === FALSE) {
      drupal_set_message(t('Quotes: insert author failed.'), 'error');
    }
    $aid = db_result(db_query("SELECT aid FROM {quotes_authors} qa WHERE qa.name='%s'", $author));
    if ($aid === FALSE) {
      drupal_set_message(t('Quotes: get aid failed.'), 'error');
    }
  }
  return $aid;
}

/**
 * Implementation of hook_update().
 */
function quotes_update($node) {
  global $user;
  $aid = _quotes_handle_author($node->quotes_author);
  if ($node->revision) {
    quotes_insert($node);
  }
  else {
    $result = db_query("UPDATE {quotes} q SET q.aid='%d', q.citation='%s', q.promote=%d WHERE q.nid=%d AND q.vid=%d", $aid, $node->quotes_citation, $node->quotes_promote, $node->nid, $node->vid);
    if ($result === FALSE) {
      drupal_set_message(t('Quotes update failed.'), 'error');
    }
  }
}

/**
 * Implementation of hook_delete().
 */
function quotes_delete($node) {
  $aid = _quotes_handle_author($node->quotes_author);

  // If this is the last quote using this author, then delete the author too.
  $aid_count = db_result(db_query("SELECT COUNT(q.nid) FROM {quotes} q WHERE q.aid='%d'", $aid));
  if ($aid_count == 1) {
    db_query("DELETE FROM {quotes_authors} WHERE aid = %d", $aid);
  }
  db_query("DELETE FROM {quotes} WHERE nid = %d", $node->nid);
}

/**
 * Implementation of hook_view().
 */
function quotes_view($node, $teaser = FALSE, $page = FALSE, $links = FALSE, $max_length = 0) {
  global $user;
  if ($page) {

    // Breadcrumb navigation.
    $breadcrumb = array();
    $breadcrumb[] = array(
      'path' => 'quotes',
      'title' => t('Quotes'),
    );
    $breadcrumb[] = array(
      'path' => "quotes/{$node->uid}",
      'title' => t("!name's quotes", array(
        '!name' => $node->uid ? $node->name : variable_get('anonymous', t('Anonymous')),
      )),
    );
    $breadcrumb[] = array(
      'path' => "node/{$node->nid}",
    );
    menu_set_location($breadcrumb);
  }

  // Prepare the node content.
  if (!isset($node->quotes_format)) {
    $node = node_prepare($node);
    $node->content['body'] = array(
      '#value' => theme('quotes_quote', $node, $teaser, FALSE, $max_length),
      '#weight' => 0,
    );
  }
  else {
    $quotes = array();
    foreach (_quotes_parse_import($node) as $quote) {
      $quote->body = check_markup($quote->body, $node->format, $user->uid == $node->uid);
      $quotes[] = theme('quotes_quote', $quote);
    }
    $node->content['body'] = array(
      '#value' => theme('item_list', $quotes, t('!count quotes will be imported:', array(
        '!count' => count($quotes),
      ))),
      '#weight' => 0,
    );
  }
  return $node;
}

/**
 * Implementation of hook_link().
 */
function quotes_link($type, $node = NULL, $teaser = FALSE) {
  global $user;
  $links = array();
  if ($type == 'node' && $node->type == 'quotes' && !(arg(0) == 'quotes' && arg(1) == $node->uid)) {
    $name = $node->uid ? $node->name : variable_get('anonymous', t('Anonymous'));
    if (variable_get('quotes_showlink', TRUE)) {
      if ($node->uid != $user->uid) {
        $links['quotes_usernames_quotes'] = array(
          'title' => t("!name's quotes", array(
            '!name' => $name,
          )),
          'href' => "quotes/{$node->uid}",
          'attributes' => array(
            'title' => t("View !name's quotes.", array(
              '!name' => $name,
            )),
          ),
        );
      }
    }
    if (variable_get('quotes_edit_link', TRUE)) {
      if (user_access('edit own quotes') && $node->uid == $user->uid || user_access('administer quotes')) {
        $links['quotes_edit_link'] = array(
          'title' => t('Edit quote'),
          'href' => 'node/' . $node->nid . '/edit',
          'attributes' => array(
            'title' => t('Edit this quote'),
          ),
        );
      }
    }
  }
  return $links;
}

/**
 * Implementation of hook_block().
 */
function quotes_block($op = 'list', $delta = 0, $edit = array()) {
  global $_quotes_subs;

  // This is done here for "configure." The actual values are set below.
  $_quotes_subs = array(
    '%' . t('interval') => NULL,
    '%' . t('bid') => NULL,
    '%' . t('title') => NULL,
    '%' . t('nid') => NULL,
    '%' . t('user') => NULL,
  );
  switch ($op) {
    case 'mb_blocked':
      return 'quotes';
    case 'list':
      $blocks = array();
      $result = db_query('SELECT qb.bid, qb.name FROM {quotes_blocks} qb');
      while ($block = db_fetch_object($result)) {
        $blocks[$block->bid] = array(
          'info' => t('Quotes') . ': ' . $block->name,
        );
      }
      return $blocks;
    case 'view':
      $block = db_fetch_array(db_query('SELECT qb.* FROM {quotes_blocks} qb WHERE qb.bid=%d', $delta));
      if (!$block) {
        return NULL;
      }
      if ($block['cron_interval'] > 0) {
        if (!$block['vid'] || $block['cron_last'] + $block['cron_interval'] * $block['cron_step'] < time()) {
          $block['vid'] = quotes_get_quote($block, TRUE, 1);
          db_query('UPDATE {quotes_blocks} SET vid=%d, cron_last=%d WHERE bid=%d', $block['vid'][0], time(), $delta);
          cache_clear_all();
        }
        else {
          $block['vid'] = array(
            $block['vid'],
          );
        }
      }
      else {
        $block['vid'] = quotes_get_quote($block, TRUE, $block['count']);
      }
      if (!$block['vid']) {
        return NULL;
      }
      $view_text = $block['view_text'];
      $link_weight = $block['view_weight'];
      $more_text = $block['more_text'];
      $rand_freq = $block['rand_freq'];
      $output = NULL;
      unset($blk_title);
      foreach ($block['vid'] as $nid) {
        $node = node_load($nid);

        // Save first node title for possible block title below
        if (!isset($blk_title)) {
          $blk_title = filter_xss($node->title);
          $blk_nid = $node->nid;
          $blk_uid = $node->uid;
        }

        // See if we don't want the citation shown.
        if (!$block['show_citation']) {
          unset($node->quotes_citation);
        }

        // We don't want bios in a block.
        unset($node->quotes_bio);

        // Quotes_view builds $node->content['body'].
        $quote = quotes_view($node, FALSE, FALSE, FALSE, $block['max_length']);

        // Do we want titles?
        switch ($block['show_titles']) {
          case 1:

            // Link to node.
            $quote->content['title'] = array(
              '#value' => '<h3>' . drupal_get_path_alias(l($node->title, 'node/' . $nid)) . '</h3>',
              '#weight' => -100,
            );
            break;
          case 2:

            // Plain text.
            $quote->content['title'] = array(
              '#value' => '<h3>' . check_plain($node->title) . '</h3>',
              '#weight' => -100,
            );
            break;
        }
        if ($node->comment && !empty($view_text)) {
          $quote->content['view_link'] = array(
            '#value' => '<div class="quotes-view-link">' . drupal_get_path_alias(l($view_text, 'node/' . $nid)) . '</div>',
            '#weight' => $link_weight,
          );
        }
        if ($more_text) {
          $quote->content['more_link'] = array(
            '#value' => '<div class="quotes-more-link">' . drupal_get_path_alias(l($more_text, 'quotes')) . '</div>',
            '#weight' => 2,
          );
        }
        $output .= drupal_render($quote->content);
      }
      $_quotes_subs = array(
        '%' . t('interval') => format_interval($block['cron_interval'] * $block['cron_step'], 1),
        '%' . t('bid') => $block['bid'],
        '%' . t('title') => $blk_title,
        '%' . t('nid') => $blk_nid,
        '%' . t('user') => theme('username', user_load(array(
          'uid' => $blk_uid,
        ))),
      );
      $subject = strtr($block['name'], $_quotes_subs);
      return array(
        'subject' => $subject,
        'content' => $output,
      );
    case 'configure':
      return _quotes_block_configure($delta, $edit);
    case 'save':
      _quotes_block_configure_save($delta, $edit);
  }
}

/**
 * Quotes block configuration.
 */
function _quotes_block_configure($delta = 0, $edit = array()) {
  global $_quotes_subs;
  drupal_add_js(drupal_get_path('module', 'quotes') . '/quotes.js');
  $block = db_fetch_object(db_query("SELECT qb.* FROM {quotes_blocks} qb WHERE bid = %d", $delta));
  $any_filters = $block->nid_filter . $block->aid_filter . $block->rid_filter . $block->uid_filter . $block->tid_filter;
  $block->aid_filter = explode(',', $block->aid_filter);
  $block->rid_filter = explode(',', $block->rid_filter);
  $block->uid_filter = explode(',', $block->uid_filter);
  $block->tid_filter = explode(',', $block->tid_filter);

  // Get authors.
  $authors = quotes_get_authors();

  // Get roles.
  $roles = user_roles(FALSE, 'create quotes');
  foreach (user_roles(FALSE, 'import quotes') as $rid => $role) {
    $roles[$rid] = $role;
  }
  foreach (user_roles(FALSE, 'edit own quotes') as $rid => $role) {
    $roles[$rid] = $role;
  }

  // Get users.
  $users = array();
  if ($roles[DRUPAL_ANONYMOUS_RID]) {
    $users[0] = variable_get('anonymous', t('Anonymous'));
  }
  $result = db_query("SELECT DISTINCT u.uid, u.name FROM {users} u LEFT JOIN {users_roles} ur ON ur.uid = u.uid WHERE u.uid = 1 OR ur.rid IN (" . implode(',', count($roles) ? array_keys($roles) : array(
    0,
  )) . ")");
  while ($row = db_fetch_object($result)) {
    $users[$row->uid] = $row->uid ? $row->name : variable_get('anonymous', t('Anonymous'));
  }
  $form = array();
  if ($delta) {
    $form['bid'] = array(
      '#type' => 'value',
      '#value' => $delta,
    );
  }
  $form['quotes'] = array(
    '#type' => 'fieldset',
    '#title' => 'Quotes specific settings',
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $form['quotes']['block_type'] = array(
    '#type' => 'radios',
    '#title' => t('Type'),
    '#required' => TRUE,
    '#default_value' => $block->block_type == 1 ? 1 : 0,
    // Note the order of these options is important!
    '#options' => array(
      t('Random'),
      t('Most recent'),
      t('Unpublished'),
    ),
    '#description' => t('"Random" will choose a published quote at random. "Most recent" will display the most recently updated quotes. "Unpublished" will display quotes that are marked as not published (awaiting approval).'),
    '#prefix' => '<div class="quotes-radios">',
    '#suffix' => '</div>',
  );
  $sub_str = implode(', ', array_keys($_quotes_subs));
  $form['quotes']['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Name'),
    '#required' => TRUE,
    '#default_value' => $block->name,
    '#description' => t("Enter a unique name for this block. This will identify the block on the !block administration page and be used for the default block title. The name may include any of: '%subs'.", array(
      '!block administration page' => l(t('block administration page'), 'admin/build/block'),
      '%subs' => $sub_str,
    )),
  );
  $title_opts = array(
    0 => t('No title'),
    1 => t('Title linked to quote'),
    2 => t('Plain text'),
  );
  $form['quotes']['show_titles'] = array(
    '#type' => 'radios',
    '#options' => $title_opts,
    '#title' => t('Show titles'),
    '#required' => FALSE,
    '#default_value' => $block->show_titles,
    '#description' => t('If this option is selected, the titles of the quotes in this block will be shown.'),
    '#prefix' => '<div class="quotes-radios">',
    '#suffix' => '</div><div class="clear-block"></div>',
  );
  $form['quotes']['show_citation'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show citation?'),
    '#default_value' => $block->show_citation,
    '#description' => t('If this option is selected, the citation for the quote will be included in blocks.'),
  );
  $form['quotes']['max_length'] = array(
    '#type' => 'select',
    '#title' => t('Maximum quote length'),
    '#options' => drupal_map_assoc(array(
      0,
      10,
      20,
      30,
      40,
      50,
      60,
      70,
      80,
      90,
      128,
      192,
      256,
      320,
      384,
      448,
      512,
    )),
    '#default_value' => $block->max_length,
    '#description' => t('This will limit the length of a quote shown in the block. A value of zero (0) means no limit.'),
  );
  $form['quotes']['block_count'] = array(
    '#type' => 'select',
    '#options' => drupal_map_assoc(array(
      1,
      2,
      3,
      4,
      5,
      6,
      7,
      8,
      9,
      10,
      15,
      20,
      25,
      30,
      40,
      50,
      75,
      100,
    )),
    '#title' => t('Number of quotes in this block'),
    '#default_value' => $block->count,
    '#description' => t('This sets the maximum number of quotes that will be displayed in this block. Note that "Random" and timed ("Update every") blocks may contain only one quote.'),
  );
  $form['quotes']['view_text'] = array(
    '#type' => 'textfield',
    '#title' => t('Block "View" link text'),
    '#maxlength' => 64,
    '#default_value' => $block->view_text,
    '#description' => t('The text of the link to view the quote from a block. Leaving this field blank disables the link.'),
  );
  $form['quotes']['view_weight'] = array(
    '#type' => 'weight',
    '#title' => t('"View" link weight'),
    '#delta' => 1,
    '#default_value' => $block->view_weight,
    '#description' => t('A positive value sets the link after the body; a negative value before.'),
  );
  $form['quotes']['block_more'] = array(
    '#type' => 'textfield',
    '#title' => t('"More" link text'),
    '#maxlength' => 64,
    '#default_value' => $block->more_text,
    '#description' => t('This sets the text that will display on the "more" link at the bottom of the block. Leave it blank for no link. This is only available for a "Random" block.'),
    '#prefix' => '<div class="quotes_block_more">',
  );
  $frequency = drupal_map_assoc(range(0, 100, 10));
  $form['quotes']['rand_freq'] = array(
    '#type' => 'select',
    '#title' => t('How often block will show'),
    '#options' => $frequency,
    '#default_value' => isset($block->rand_freq) ? $block->rand_freq : 100,
    '#description' => t('This sets the frequency with which the block will be shown. 100% is all the time; 0% is none of the time.'),
    //    '#prefix' => '<div class="quotes_block_more">',
    '#suffix' => '</div>',
  );
  $form['quotes']['filters'] = array(
    '#type' => 'fieldset',
    '#title' => 'Quotes Filters',
    '#collapsible' => TRUE,
    '#collapsed' => empty($any_filters),
  );
  $form['quotes']['filters']['nid_filter'] = array(
    '#type' => 'textarea',
    '#title' => t('Node filter'),
    '#rows' => 2,
    '#default_value' => $block->nid_filter,
    '#description' => t('To restrict this block to display only certain quotes based on node IDs, enter the IDs here separated by commas, spaces, or returns.'),
  );
  if (count($authors)) {
    $form['quotes']['filters']['aid_filter'] = array(
      '#type' => 'select',
      '#title' => t('Author filter'),
      '#multiple' => TRUE,
      '#default_value' => $block->aid_filter,
      '#options' => $authors,
      '#description' => t('To restrict this block to display only quotes from certain authors, select the authors here.'),
    );
  }
  else {
    $form['quotes']['filters']['aid_filter'] = array(
      '#type' => 'item',
      '#title' => t('Author filter'),
      '#description' => t('There are no authors.  To filter by authors, there must be at least one quote with an attributed author'),
    );
  }
  if (count($roles)) {
    $form['quotes']['filters']['rid_filter'] = array(
      '#type' => 'select',
      '#title' => t('Role filter'),
      '#multiple' => TRUE,
      '#default_value' => $block->rid_filter,
      '#options' => $roles,
      '#description' => t('To restrict this block to display only quotes submitted by users in specific roles, select the roles here.'),
    );
  }
  else {
    $form['quotes']['filters']['rid_filter'] = array(
      '#type' => 'item',
      '#title' => t('Role filter'),
      '#description' => t('There are no roles configured with the %create quotes, %import quotes, or %edit own quotes permissions, so no roles are available. To filter by role, please assign this permission to at least one role on the !access control page.', array(
        '%create quotes' => t('create quotes'),
        '%import quotes' => t('import quotes'),
        '%edit own quotes' => t('edit own quotes'),
        '!access control page' => l(t('access control page'), 'admin/user/access'),
      )),
    );
  }
  $form['quotes']['filters']['uid_filter'] = array(
    '#type' => 'select',
    '#title' => t('User filter'),
    '#multiple' => TRUE,
    '#default_value' => $block->uid_filter,
    '#options' => $users,
    '#description' => t('To restrict this block to display only quotes submitted by specific users, select the users here.'),
  );
  if (function_exists('taxonomy_form_all')) {
    $form['quotes']['filters']['tid_filter'] = array(
      '#type' => 'select',
      '#title' => t('Category filter'),
      '#multiple' => TRUE,
      '#default_value' => $block->tid_filter,
      '#options' => taxonomy_form_all(TRUE),
      '#description' => t('To restrict this block to display only quotes in specific categories, select the categories here.'),
    );
  }
  $form['quotes']['cron'] = array(
    '#type' => 'fieldset',
    '#title' => t('Update options'),
    '#description' => t('Note: Timed blocks will contain only one quote.'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $form['quotes']['cron']['cron_interval'] = array(
    '#type' => 'textfield',
    '#size' => 4,
    '#maxlength' => 3,
    '#default_value' => $block->cron_interval ? $block->cron_interval : '',
    '#field_prefix' => t('Update every'),
    '#prefix' => '<div class="container-inline">',
  );
  $form['quotes']['cron']['cron_step'] = array(
    '#type' => 'select',
    '#default_value' => $block->cron_step,
    '#options' => array(
      60 => t('minutes'),
      60 * 60 => t('hours'),
      60 * 60 * 24 => t('days'),
      60 * 60 * 24 * 7 => t('weeks'),
    ),
    '#suffix' => '</div>',
  );
  $form['quotes']['cron']['description'] = array(
    '#type' => 'item',
    '#description' => t('If set, the quote displayed in this block will get updated based on the interval specified (requires cron if page cache is enabled). Leave this value blank to have the quote updated every time the block is viewed.'),
    '#prefix' => '<div style="display: block;">',
    '#suffix' => '</div>',
  );
  return $form;
}

/**
 * Quotes block configuration save.
 */
function _quotes_block_configure_save($delta, $edit) {
  $vals = array(
    $edit['name'],
    $edit['block_type'],
    preg_replace('<[,\\s]+>', ',', trim($edit['nid_filter'])),
    implode(',', (array) $edit['aid_filter']),
    implode(',', (array) $edit['rid_filter']),
    implode(',', (array) $edit['uid_filter']),
    implode(',', (array) $edit['tid_filter']),
    $edit['cron_interval'] ? $edit['cron_interval'] : 0,
    $edit['cron_step'],
    // Make sure random and timed blocks have a count of 1.
    $edit['block_type'] == 0 || !empty($edit['cron_interval']) ? 1 : $edit['block_count'],
    $edit['show_titles'],
    $edit['show_citation'],
    // We only save the "more" text for random blocks.
    $edit['block_type'] == 0 ? $edit['block_more'] : NULL,
    $edit['view_text'],
    $edit['view_weight'],
    $edit['max_length'],
    // Frequency is only for random blocks.
    $edit['block_type'] == 0 ? $edit['rand_freq'] : 100,
    $delta,
  );
  db_query("UPDATE {quotes_blocks} SET name = '%s', block_type = %d, nid_filter = '%s', aid_filter = '%s', rid_filter = '%s',\n    uid_filter = '%s', tid_filter = '%s', cron_interval = %d, cron_step = %d,\n    count = %d, show_titles = %d, show_citation = %d, more_text = '%s',\n    view_text = '%s', view_weight = %d, max_length = %d, rand_freq = %d WHERE bid = %d", $vals);
}

/**
 * Implementation of hook_form_alter().
 */
function quotes_form_alter($form_id, &$form) {
  if ($form_id == 'block_admin_configure' && strpos($_GET['q'], 'admin/build/block/configure/quotes') == 0) {
    $form['#validate']['_quotes_block_configuration_validate'] = current($form['#validate']);
  }
}

/**
 * Implementation of hook_cron().
 */
function quotes_cron() {
  $result = db_query("SELECT qb.* FROM {quotes_blocks} qb INNER JOIN {blocks} b ON b.module = 'quotes' WHERE b.status = 1 AND qb.cron_interval > 0 AND (qb.vid = 0 OR (qb.cron_last + (qb.cron_step * qb.cron_interval)) < %d)", time());
  for ($updated = FALSE; $block = db_fetch_array($result); $updated = TRUE) {
    $quotes = quotes_get_quote($block, TRUE, 1);
    db_query('UPDATE {quotes_blocks} SET vid = %d, cron_last = %d WHERE bid = %d', $quotes[0], time(), $block['bid']);
  }
  if ($updated) {
    cache_clear_all();
  }
}

/**
 * Implementation of hook_help().
 */
function quotes_help($path, $args = NULL) {
  switch ($path) {
    case 'node/add#quotes':
      return t('A quote is a famous, infamous, humorous, witty, or otherwise noteworthy quotation or fortune file entry. Quotes can be entered one at a time or mass imported in either tab-separated text or fortune file format.');
    case 'node/add/quotes':
      return t('Use the form below to enter a single quote.') . (user_access('import quotes') ? ' ' . t('Multiple quotes can also be !mass imported in either tab-separted text or fortune file format.', array(
        '!mass imported' => l(t('mass imported'), 'node/add/quotes/import'),
      )) : '');
    case 'node/add/quotes/import':
      $output = t('<p>Use the form below to mass import quotes in either tab-separated text or fortune file format.
        Many quotes will be imported in this one step by creating an individual node for each imported quote.
        Expand the %Notes section below for more information.</p>', array(
        '%Notes' => t('Notes'),
      ));
      $output .= theme('fieldset', array(
        '#title' => t('Notes for importing'),
        '#collapsible' => TRUE,
        '#collapsed' => TRUE,
        '#value' => t('<ul>
  <li>Tab-separated quotes should appear one quote per line in the format
  <em><span style="color: blue;">quote</span><span style="color: red;">&lt;tab&gt;</span><span style="color:
    green;">author</span></em>. The author and citation are optional; however, the tab is still required.
    To import quotes, authors, or citations with more than one line, escape the embedded newlines with a backslash.
    Examples:

  <pre style="font-size: .75em;">
<span style="color: blue;">Single-line quote.</span><em style="color: red;">&lt;tab&gt;</em><span style="color: green;">Author</span>
<span style="color: blue;">Quote without author.</span><em style="color: red;">&lt;tab&gt;</em>
<span style="color: blue;">Multi-line quote: line 1...\\
...line 2.</span><em style="color: red;">&lt;tab&gt;</em><span style="color: green;">Author line 1\\
Author line 2</span>
<span style="color: blue;">Another quote.<em style="color: red;">&lt;tab&gt;</em><span style="color: green;">Another Author</span>
  </pre></li>
  <li>Fortune files do not explicitly provide an author or attribution
    for each quote/fortune. This import will extract an author when
    there is a line of the form <em>-- Author</em> with any amount of
    leading whitespace.  Examples:

  <pre style="font-size: .75em;">
<span style="color: blue;">A fortune without an author.</span>
<em style="color: red;">%</em>
<span style="color: blue;">Fortune with author.</span>
--<span style="color: green;">Author</span>
<em style="color: red;">%</em>
<span style="color: blue;">Multi-line fortune: line 1...
...line 2.</span>
          -- <span style="color: green;">Author line 1
             Author line 2</span>
  </pre></li>
  <li>Any settings used in the form below (comment, moderation, sticky,
    input format, categories, etc.) will be applied to all imported
    quotes.</li>
  <li>The title entered below will be applied to all quotes. You can use
    the variable <em>%id</em> in the title which will be replaced by
    the newly-created quote\'s node ID.</li>
  <li>Fortune files and tab-separated text data can contain many
    quotes. To avoid timeout errors while importing large amounts of
    data, consider importing in smaller chunks with no more than 1000
    quotes at a time.</li>
  <li>If the path module is enabled, you cannot create a path alias
    while importing, as the import will attempt to use the same path
    for every quote.</li>
</ul>
'),
      ));
      return $output;
    case 'admin/settings/quotes':
      $text = t('This page displays the status of, and settings for, the quotes module. The permissions for this module are found <a href="!url">here</a>.', array(
        '!url' => url('admin/user/access'),
      ));
      $text .= '<p>Version: ' . QUOTES_VERSION . '</p>';
      return $text;
    case 'admin/settings/quotes/blocks':
      return t('You can define any number of blocks that will each display a randomly-selected quote,
the most-recent quotes, or unpublished quotes. The quotes displayed
in each block can be restricted to certain node IDs, roles, users, or
categories. Each block has a name that is used for identification on
on the !block administration page and as the default title when the
block is displayed.', array(
        '!block administration page' => l(t('block administration page'), 'admin/build/block'),
      ));
    case 'admin/settings/quotes/add':
      return t('Use the form below to define a new quote block.');
    case 'admin/help#quotes':
      return t('The quotes module allows users to maintain a list of quotations that
they find notable, humorous, famous, infamous, or otherwise worthy of
sharing with website visitors. The quotes can be displayed in any
number of administrator-defined blocks. These blocks will display
quotes based on the restrictions of each block. Blocks can be
configured to restrict to certain nodes, roles, users, or categories.');
    case 'user/' . arg(1) . '/quotes':
    case 'quotes/author/' . arg(1):
    case 'quotes/author':
    case 'quotes/' . arg(1):
    case 'quotes':
      $links = array();
      if (user_access('create quotes')) {
        $links['add'] = array(
          'title' => t('Add another'),
          'href' => 'node/add/quotes',
        );
      }
      if (user_access('administer quotes')) {
        $links['admin'] = array(
          'title' => t('Settings'),
          'href' => 'admin/settings/quotes',
          'query' => drupal_get_destination(),
        );
      }
      return theme('links', $links);
  }
}

/**
 * Trim text to the admin-specified length.
 */
function _quotes_trim($text, $length = NULL) {
  if (!$length) {

    // Blocks have to specify the length.
    $length = variable_get('weblinks_trim', 0);
  }

  // Zero means no limit;
  if ($length == 0) {
    return $text;
  }

  // Use +3 for '...' string length.
  if (drupal_strlen($text) > $length + 3) {
    $text = drupal_substr($text, 0, $length) . '...';
  }
  return $text;
}

//********************************************************************

//* Themeable Functions

//********************************************************************

/**
 * Themeable function that displays a single quote and optional author.
 *
 * @param $node
 *   The node object containing the quote body and author.
 * @param $teaser
 *   Boolean to indicate whether to use the teaser or the body.
 * @param $bio
 *   Boolean to indicate whether to use the author's biography.
 *
 * @return
 *   An HTML-formatted quote.
 *
 * @ingroup themeable
 */
function theme_quotes_quote($node, $teaser = FALSE, $bio = TRUE, $max_length = 0) {
  global $user;
  if ($max_length) {

    // We want to limit the text.
    $text = _quotes_trim($node->body, $max_length);
    if (drupal_strlen($text) < drupal_strlen($node->body)) {
      $text .= l(t('(more)'), drupal_get_path_alias('node/' . $node->nid));
    }
  }
  else {
    $text = $teaser ? $node->teaser : $node->body;
  }
  $body = check_markup($text, $node->format, $user->uid == $node->uid);
  $leader = variable_get('quotes_leader', '&mdash;') . ' ';
  if (arg(1) == 'author') {
    $show_bio = FALSE;
  }
  else {
    $show_bio = variable_get('quotes_author_bio', FALSE);
  }
  if (variable_get('quotes_author_link', FALSE)) {
    $author = $node->quotes_author ? $leader . l($node->quotes_author, 'quotes/author/' . $node->quotes_author, array(
      'title' => t('View all quotes by this author'),
    )) : '';
    $author = decode_entities($author);
  }
  else {
    $author = $node->quotes_author ? check_markup($leader . $node->quotes_author, $node->format, $user->uid == $node->uid) : '';
  }
  switch ($show_bio) {
    case 1:
      $bio = $node->quotes_bio ? '<div class="quotes-bio">' . check_markup($node->quotes_bio, $node->format, $user->uid == $node->uid) . '</div>' : '';
      break;
    case 2:
      $bio = $node->quotes_author ? '<div class="quotes-bio-link">' . l(t('See biography and quotes'), 'quotes/author/' . $node->quotes_author, array(
        'title' => t('View all quotes by this author'),
      )) . '</div>' : '';
      break;
    default:
      $bio = NULL;
  }
  $citation = $node->quotes_citation ? check_markup("<cite>" . $node->quotes_citation . "</cite>", $node->format, $user->uid == $node->uid) : '';
  return '<div class="quotes-quote">' . $body . '</div>' . ($author ? '<div class="quotes-author">' . $author . '</div>' : '') . $bio . ($citation ? '<div class="quotes-citation">' . $citation . '</div>' : '') . '<div class="clear-block"></div>';
}

/**
 * Themeable function that displays a page of quotes that may be
 * restricted to a certain user.
 *
 * @param $uid
 *   The user ID of the user whose quotes should be displayed.
 *
 * @return
 *   An HTML-formatted list of quotes.
 *
 * @ingroup themeable
 */
function theme_quotes_page($uid) {
  $limit = variable_get('quotes_per_page', 10);
  if (isset($uid)) {
    $user = user_load(array(
      'uid' => $uid,
      'status' => 1,
    ));
    $name = $uid ? module_exists('realname') ? $user->realname : $user->name : variable_get('anonymous', t('Anonymous'));
    drupal_set_title(t("!name's quotes", array(
      '!name' => $name,
    )));
    $url = url("quotes/{$uid}/feed");
    $result = pager_query(db_rewrite_sql("SELECT n.nid FROM {node} n INNER JOIN {node_revisions} nr USING (vid) WHERE n.status=1 AND n.type='quotes' AND n.uid=%d ORDER BY n.sticky DESC, n.created DESC"), $limit, 0, NULL, $uid);
  }
  else {
    drupal_set_title(t('Quotes'));
    $url = url('quotes/feed');
    $result = pager_query(db_rewrite_sql("SELECT n.nid, n.sticky FROM {node} n INNER JOIN {node_revisions} nr USING (vid) WHERE n.status = 1 AND n.type = 'quotes' ORDER BY n.sticky DESC, n.created DESC"), $limit);
  }
  $output = '<div class="quotes">';
  while ($node = db_fetch_object($result)) {
    $quote = node_load($node->nid);
    $output .= node_view($quote, TRUE);
  }
  $output .= theme('pager', NULL, $limit);
  drupal_add_feed($url, t('RSS - !title', array(
    '!title' => drupal_get_title(),
  )));
  return $output . '</div>';
}

//********************************************************************

//* Module Functions

//********************************************************************

/**
 * Returns the node ID for either a random quote or the most recent
 * quote based on the provided filter criteria.
 *
 * @param $filters
 *   The array specifying filter criteria to be passed to
 *   quotes_block_join_sql() and quotes_block_where_sql().
 * @param $promoted_only
 *   The boolean specifying whether or not only promoted quotes should
 *   be returned.
 * @param $limit
 *   The number of quotes to retrieve.
 *   Note that random quote blocks will only retrieve one quote.
 *
 * @return
 *   An array of node IDs for quotes matching the specified criteria.
 */
function quotes_get_quote($filters = array(), $promoted_only, $limit = 1) {
  if ($filters['block_type'] == 0) {

    // Random block, see if we want to display it this time?
    if (isset($filters['rand_freq']) && $filters['rand_freq'] < 100) {

      // Check against a random number.
      if ($filters['rand_freq'] < rand(0, 100)) {

        // Nope, not this time.
        return array();
      }
    }
  }
  $query = 'SELECT n.nid FROM {quotes} q INNER JOIN {node} n ON q.vid=n.vid ' . quotes_block_join_sql($filters) . 'WHERE n.status=' . (int) ($filters['block_type'] != 2) . " AND n.type='quotes' AND " . ($promoted_only ? ' q.promote = 1 AND ' : '') . quotes_block_where_sql($filters) . ' ORDER BY ' . ($filters['block_type'] == 0 ? 'RAND()' : 'n.created DESC');

  /* Type=0 is random. */
  $result = db_query_range(db_rewrite_sql($query), 0, $limit);
  $ret = array();
  while ($row = db_fetch_array($result)) {
    $ret[] = $row['nid'];
  }
  return $ret;
}

/**
 * Returns the SQL join text necessary for the provided filter
 * criteria.
 *
 * @param $filters
 *   The array specifying filter criteria using the keys nid_filter,
 *   aid_filter, rid_filter, uid_filter, and tid_filter.
 * @param $aliases
 *   The array specifying the aliases to be used for the tables that
 *   may be joined based on the provided filter criteria. Keys are the
 *   table names node, users_roles, users, and term_node.
 *
 * @return
 *   A string containing the SQL join text necessary for the provided
 *   criteria.
 */
function quotes_block_join_sql($filters = array(), $aliases = array(
  'node' => 'n',
  'users_roles' => 'qur',
  'users' => 'qu',
  'term_node' => 'qtn',
)) {
  $join = '';
  if ($filters['rid_filter']) {
    $join .= " LEFT JOIN {users_roles} {$aliases['users_roles']} ON {$aliases['users_roles']}.uid = {$aliases['node']}.uid ";
  }
  if ($filters['tid_filter']) {
    $join .= " INNER JOIN {term_node} {$aliases['term_node']} ON {$aliases['term_node']}.nid = {$aliases['node']}.nid ";
  }
  return $join;
}

/**
 * Returns the SQL where text necessary for the provided filter
 * criteria.
 *
 * @param $filters
 *   The array specifying filter criteria using the keys nid_filter,
 *   rid_filter, uid_filter, and tid_filter.
 * @param $aliases
 *   The array specifying the aliases used for the tables that are
 *   being joined in the query. Keys are the table names node,
 *   users_roles, users, and term_node.
 *
 * @return
 *   A string containing the SQL where text necessary for the provided
 *   criteria.
 */
function quotes_block_where_sql($filters = array(), $aliases = array(
  'node' => 'n',
  'users_roles' => 'qur',
  'users' => 'qu',
  'term_node' => 'qtn',
)) {
  $where = array();
  if ($filters['nid_filter']) {
    $where[] = " {$aliases['node']}.nid IN ({$filters['nid_filter']}) ";
  }
  if ($filters['aid_filter']) {
    $where[] = " (q.aid IN ({$filters['aid_filter']})) ";
  }
  if ($filters['rid_filter']) {
    $where[] = sprintf(" ({$aliases['users_roles']}.rid IN ({$filters['rid_filter']}) OR (%d IN ({$filters['rid_filter']}) AND {$aliases['node']}.uid = 0)) ", DRUPAL_ANONYMOUS_RID);
  }
  if ($filters['uid_filter'] != '') {
    $where[] = " {$aliases['node']}.uid IN ({$filters['uid_filter']}) ";
  }
  if ($filters['tid_filter']) {
    $where[] = " {$aliases['term_node']}.tid IN ({$filters['tid_filter']}) ";
  }
  return $where ? implode(' AND ', $where) : '1=1';
}

/**
 * Menu callback that calls theme_quotes_page().
 *
 * @param $uid
 *   The user ID of the user's quotes to be displayed.
 * @param $arg2
 *   If 'feed', a feed is requested.
 *
 * @return
 *   An HTML-formatted list of quotes.
 */
function _quotes_page($uid = NULL, $arg2 = NULL) {
  global $user;
  if ($arg2 == 'feed') {
    _quotes_feed_user($uid);
  }
  else {
    return theme('quotes_page', $uid);
  }
}

/**
 * Menu callback that selects quotes based on author.
 *
 * @param $author
 *   The name or aid of the author of the quotes.
 *
 * @return
 *   An HTML-formatted list of quotes.
 */
function quotes_author($author = NULL) {
  if (!$author) {

    // If no author given, display a selection list.
    return drupal_get_form('quotes_author_form');
  }
  elseif ($author == t('unspecified')) {
    $aid = 0;
    $auth = array(
      'name' => $author,
      'bio' => '',
    );
  }
  else {

    // See whether we have a name or an id.
    if (is_numeric($author)) {
      $aid = $author;
      $auth = db_fetch_array(db_query("SELECT name, bio FROM {quotes_authors} WHERE aid=%d", $aid));
      $author = $auth['name'];
    }
    else {
      $auth = db_fetch_array(db_query("SELECT aid, bio FROM {quotes_authors} WHERE name LIKE('%s%')", $author));
      $aid = $auth['aid'];
    }
    if (!$aid) {
      return t("I couldn't locate that author.") . drupal_get_form('quotes_author_form');
    }
  }

  // Unspecified author.
  $limit = variable_get('quotes_per_page', 10);
  drupal_set_title(decode_entities(t('Quotes by @name', array(
    '@name' => $author,
  ))));
  if ($auth['bio']) {
    $output = '<div class="quotes-header-bio clear-block">' . check_markup($auth['bio']) . '</div>';
  }
  $result = pager_query(db_rewrite_sql("SELECT n.nid FROM {node} n INNER JOIN {node_revisions} nr USING (vid) INNER JOIN {quotes} q USING (vid) WHERE q.aid='%d' AND n.status=1 AND n.type='quotes' ORDER BY n.sticky DESC, n.created DESC"), $limit, 0, NULL, $aid);
  $output .= '<div class="quotes">';
  while ($node = db_fetch_object($result)) {
    $output .= node_view(node_load($node->nid), FALSE);
  }
  $output .= theme('pager', NULL, $limit);
  return $output . '</div>';
}

/**
 * Form to select an author.
 */
function quotes_author_form() {
  $form = array();
  $max_name_length = 78;
  $authors = quotes_get_authors();
  $author_names = array();
  foreach ($authors as $aid => $name) {

    // Make it safe to display (with no HTML).
    $name = filter_xss($name, array());

    // Limit the length.
    if (drupal_strlen($name) > $max_name_length) {
      $name = drupal_substr($name, 0, $max_name_length - 3) . '...';
    }
    $author_names[$aid] = $name;
  }
  $form['author'] = array(
    '#type' => 'select',
    '#options' => $author_names,
    '#size' => min(25, count($authors)),
    '#description' => t('Select an author from the list.'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Select'),
  );
  return $form;
}

/**
 * Handle submission of the form to select an author.
 */
function quotes_author_form_submit($form_id, $form_values) {
  drupal_goto('quotes/author/' . $form_values['author']);
}

/**
 * Displays an RSS feed containing recent quotes of a given user.
 *
 * @param $uid
 *   The user ID of the user's quotes to be fed.
 */
function _quotes_feed_user($uid) {
  global $user;
  $luser = $uid ? user_load(array(
    'uid' => $uid,
    'status' => 1,
  )) : $user;
  $result = db_query_range(db_rewrite_sql("SELECT n.nid, nr.title, nr.teaser, n.created, u.name, u.uid FROM {node} n INNER JOIN {node_revisions} nr ON nr.vid = n.vid INNER JOIN {users} u ON u.uid = n.uid WHERE n.status = 1 AND n.type = 'quotes' AND u.uid = %d ORDER BY n.created DESC"), $luser->uid, 0, 15);
  node_feed($result, array(
    'title' => t("!name's quotes", array(
      '!name' => $luser->uid ? $luser->name : variable_get('anonymous', t('Anonymous')),
    )),
    'link' => url("quotes/{$luser->uid}", NULL, NULL, TRUE),
  ));
}

/**
 * Displays an RSS feed containing recent quotes of all users.
 */
function _quotes_feed_last() {
  $result = db_query_range(db_rewrite_sql("SELECT n.nid, nr.title, nr.teaser, n.created, u.name, u.uid FROM {node} n INNER JOIN {node_revisions} nr ON nr.vid = n.vid INNER JOIN {users} u ON u.uid = n.uid WHERE n.status = 1 AND n.type = 'quotes' ORDER BY n.created DESC"), 0, 15);
  node_feed($result, array(
    'title' => variable_get('site_name', 'drupal') . ' ' . t('quotes'),
    'link' => url("quotes", NULL, NULL, TRUE),
  ));
}

/**
 * Parses and returns the quotes contained in the provided node body.
 *
 * @param $node
 *   The node object containing the data to be parsed.
 * @param $set_errors
 *   The boolean indicating whether or not form errors should be set.
 *
 * @return
 *   An array containing the parsed quotes as objects with properties
 *   body, quotes_author, quotes_citation, and format.
 */
function _quotes_parse_import($node, $set_errors = FALSE) {
  $quotes = array();
  if ($node->quotes_format == 'text') {

    // The documentation shows '<tab>' and some users have actually used that string, so let's allow it.
    $node->body = str_replace('<tab>', "\t", $node->body);
    foreach (explode("\r", str_replace("\\\r", "\n", preg_replace('<(?:\\r\\n?|\\n)>', "\r", trim($node->body)))) as $quote) {
      $quote = explode("\t", $quote);
      if (count($quote) < 2 || !trim($quote[0])) {
        if ($set_errors) {
          form_set_error('body', t('Parse error on quote !num.', array(
            '!num' => count($quotes) + 1,
          )));
        }
        break;
      }
      $new_quote = drupal_clone($node);
      $new_quote->body = trim($quote[0]);
      $new_quote->quotes_author = $quote[1];
      $new_quote->quotes_citation = $quote[2];
      $quotes[] = $new_quote;
    }
  }
  elseif ($node->quotes_format == 'fortune') {
    foreach (preg_split('<\\n+%+\\n+>', str_replace("\t", '    ', preg_replace('<(?:\\r\\n?|\\n)>', "\n", $node->body))) as $quote) {
      if (preg_match('<^(?:(?:(.*)\\n+\\s*--\\s*(.*?)))$>s', $quote, $matches)) {
        if (!trim($matches[1])) {
          if ($set_errors) {
            form_set_error('body', t('Parse error on quote !num.', array(
              '!num' => count($quotes) + 1,
            )));
          }
          break;
        }
        $quotes[] = (object) array(
          'body' => trim($matches[1]),
          'quotes_author' => $matches[2],
          'quotes_citation' => NULL,
          'format' => $node->format,
        );
      }
      else {
        if (!trim($quote)) {
          if ($set_errors) {
            form_set_error('body', t('Parse error on quote !num.', array(
              '!num' => count($quotes) + 1,
            )));
          }
          break;
        }
        $quotes[] = (object) array(
          'body' => trim($quote),
          'format' => $node->format,
        );
      }
    }
  }
  elseif ($set_errors) {
    form_set_error('quotes_format', t('Please select a valid import format.'));
  }
  return $quotes;
}

/**
 * Displays the admin settings form.
 */
function _quotes_admin_settings() {
  $form = array();
  $count = db_result(db_query("SELECT COUNT(*) FROM {node} n where n.type='quotes' AND n.status=1"));
  $form['count'] = array(
    '#type' => 'item',
    '#value' => t('There are currently !count published quotations.', array(
      '!count' => $count,
    )),
  );
  $form['display'] = array(
    '#type' => 'fieldset',
    '#title' => t('Display Options'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
    '#description' => t('These options are for general theming.'),
  );
  $form['display']['quotes_leader'] = array(
    '#type' => 'textfield',
    '#title' => t('Author leader'),
    '#default_value' => variable_get('quotes_leader', '&mdash;'),
    '#description' => t('The text placed before the author attribution (e.g. "&amp;mdash;" for an em-dash or "&amp;#8226;" for a bullet).'),
  );
  $form['display']['quotes_author_link'] = array(
    '#type' => 'checkbox',
    '#title' => t('Make author a link'),
    '#default_value' => variable_get('quotes_author_link', FALSE),
    '#description' => t('If selected, the author text will be a link to show all quotes by that author.'),
  );
  $form['display']['quotes_author_bio'] = array(
    '#type' => 'radios',
    '#options' => array(
      0 => t("Don't display"),
      1 => t('Include text'),
      2 => t('Include as a link'),
    ),
    '#title' => t("Include author's bio"),
    '#default_value' => variable_get('quotes_author_bio', FALSE),
    '#description' => t("If selected, the author's biography will be shown on the Quotes page."),
  );
  $form['display']['quotes_per_page'] = array(
    '#type' => 'textfield',
    '#title' => t('Quotes per page'),
    '#size' => 6,
    '#default_value' => variable_get('quotes_per_page', 10),
    '#description' => t('The number of quotes included on a page.'),
  );
  $form['display']['quotes_edit_link'] = array(
    '#type' => 'checkbox',
    '#title' => t('Add an "edit" link'),
    '#default_value' => variable_get('quotes_edit_link', TRUE),
    '#description' => t('If selected, an "edit" link will be shown for each quote.'),
  );
  $form['myquotes'] = array(
    '#type' => 'fieldset',
    '#title' => t('My Quotes links'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $form['myquotes']['quotes_showlink'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show link to users\' quotes'),
    '#default_value' => variable_get('quotes_showlink', TRUE),
    '#description' => t('Uncheck this to disable the link to each users\' "my quotes" page when viewing a node.'),
  );
  $form['myquotes']['quotes_show_myquotes'] = array(
    '#type' => 'checkbox',
    '#title' => t('Show the "my quotes" menu item'),
    '#default_value' => variable_get('quotes_show_myquotes', 1),
    '#description' => t('Uncheck this to disable the "my quotes" menu item for all users.'),
  );
  $form['user'] = array(
    '#type' => 'fieldset',
    '#title' => t('User Options'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
    '#description' => t('These options are for users.'),
  );
  $form['user']['quotes_user_recent'] = array(
    '#type' => 'checkbox',
    '#title' => t('Add a "Recent quotes" link on the "My account" page?'),
    '#default_value' => variable_get('quotes_user_recent', flase),
  );
  return system_settings_form($form);
}

/**
 *  Export function page.
 *
 * @return
 *   A form with a tab-delimited list of quotes.
 */
function quotes_export() {
  $form['intro'] = array(
    '#type' => 'item',
    '#title' => t('Copy and paste this list to the receiving site.'),
  );
  $output = NULL;
  $count = 0;
  $sql = db_rewrite_sql("SELECT nr.body, nr.title, qa.name AS author, q.citation FROM {node} n INNER JOIN {node_revisions} nr USING (vid) INNER JOIN {quotes} q USING (vid) INNER JOIN {quotes_authors} qa USING (aid) WHERE n.status=1 AND n.type='quotes' ORDER BY qa.name");
  $result = db_query($sql);
  while ($row = db_fetch_object($result)) {
    ++$count;
    $output .= _quotes_escape_newlines($row->body) . "\t" . _quotes_escape_newlines($row->author) . "\t" . _quotes_escape_newlines($row->citation) . "\n";
  }
  drupal_set_message(t('Found !count quotes.', array(
    '!count' => $count,
  )));
  if ($count == 0) {
    $count = 1;
    $output = t('No quotes were found.');
  }
  $form['list'] = array(
    '#type' => 'textarea',
    '#value' => $output,
    '#rows' => min(30, $count),
  );
  return $form;
}

/**
 *  Bios maintenance page.
 *
 * @return
 *   A form with a tab-delimited list of quotes.
 */
function quotes_bios($aid = NULL) {
  $form = array();
  $first_pass = is_null($aid);
  if ($first_pass) {
    $auths = quotes_get_authors();
    $count = count($auths);
    $data = array(
      'name' => NULL,
      'bio' => NULL,
    );
  }
  else {

    // Get the data.
    $data = db_fetch_array(db_query("SELECT * FROM {quotes_authors} WHERE aid=%d", $aid));
  }
  $form['aid'] = array(
    '#type' => 'hidden',
    '#value' => $aid,
  );
  $form['author'] = array(
    '#type' => 'select',
    '#options' => $auths,
    '#size' => min(20, $count),
    '#description' => t('Pick the author whose biography you wish to update.'),
  );
  $form['name'] = array(
    '#type' => 'markup',
    '#value' => '<h3>' . t('Biography for %name', array(
      '%name' => $data['name'],
    )) . '</h3>',
  );
  $form['bio'] = array(
    '#type' => 'textarea',
    '#description' => t("This is the author's biography."),
    '#default_value' => $data['bio'],
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => $first_pass ? t('Get biography') : t('Update'),
  );

  // Show the fields at the right time.
  if ($first_pass) {
    $form['name']['#type'] = 'hidden';
    $form['bio']['#type'] = 'hidden';
  }
  else {
    $form['author']['#type'] = 'hidden';
  }
  return $form;
}
function quotes_bios_submit($form_id, $form_values) {

  //watchdog('quotes', 'aid = '. $form_values['aid'] .', bio = '. $form_values['bio'] .', author = '. $form_values['author'] .'.');
  if ($form_values['op'] == 'Update') {
    $vals = array(
      $form_values['bio'],
      $form_values['aid'],
    );
    $upd = db_query("UPDATE {quotes_authors} SET bio='%s' WHERE aid=%d", $vals);
    return BIOS_PATH;
  }
  else {
    return BIOS_PATH . '/' . $form_values['author'];
  }
}

/**
 * Scans a string of text to remove new line chracters.
 *
 * @param $text
 *   The text to be scanned.
 *
 * @return
 *   The text with newlines escaped.
 */
function _quotes_escape_newlines($text) {

  // Get rid of Windows crap.
  $text = str_replace("\r", '', $text);
  return str_replace("\n", "\\\n", $text);
}

/**
 * Validates that changes made on the block configuration screen are valid.
 *
 * @param $form_id
 *   The string specifying the form ID of the form that was submitted.
 * @param $form_values
 *   The array specifying the form values.
 */
function _quotes_block_configuration_validate($form_id, $form_values) {
  if (trim($form_values['nid_filter']) && !preg_match('<^(\\d+[,\\s]*)+$>', trim($form_values['nid_filter']))) {
    form_set_error('nid_filter', t('Please enter valid node IDs.'));
  }
  $interval = $form_values['cron_interval'];
  if ($interval != '' && (!preg_match('<^\\d+$>', $interval) || $interval < 1 || $interval > 999)) {
    form_set_error('cron_interval', t('The update interval must be between 1 and 999.'));
  }
}

/**
 * Displays a list of currently-defined quote blocks.
 *
 * @return
 *   An array containing the form elements to be displayed.
 */
function _quotes_blocks() {
  $form = array();
  $form['name'] = array(
    '#type' => 'textfield',
    '#size' => 32,
    '#maxlength' => 64,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Add block'),
  );
  return $form;
}

/**
 * Validates that the new block name is valid.
 *
 * @param $form_id
 *   The string specifying the form ID of the form that was submitted.
 * @param $form_values
 *   The array specifying the form values.
 */
function _quotes_blocks_validate($form_id, $form_values) {
  $name = trim($form_values['name']);
  if (!$name) {
    form_set_error('name', t('You must specify a valid block name.'));
  }
  else {
    if (db_result(db_query("SELECT COUNT(*) FROM {quotes_blocks} qb WHERE qb.name = '%s'", $name))) {
      form_set_error('name', t('The block name %name already exists. Please choose another block name.', array(
        '%name' => $name,
      )));
    }
  }
}

/**
 * Creates the new quote block.
 *
 * @param $form_id
 *   The string specifying the form ID of the form that was submitted.
 * @param $form_values
 *   The array specifying the form values.
 */
function _quotes_blocks_submit($form_id, $form_values) {
  db_query("INSERT INTO {quotes_blocks} (bid, name, block_type, nid_filter, aid_filter, rid_filter, uid_filter, tid_filter, cron_interval, cron_step, cron_last, vid) VALUES (%d, '%s', 0, '', '', '', '', '', 0, 0, 0, 0)", db_next_id('{quotes_blocks}_bid'), trim($form_values['name']));
}

/**
 * Renders the quote block list, including the "Add block" row.
 *
 * @param $form
 *   The array specifying the form to be rendered.
 *
 * @result
 *   The formatted HTML table of blocks.
 */
function theme__quotes_blocks($form) {
  $header = array(
    t('Name'),
    t('Filters'),
    array(
      'data' => t('Operations'),
      'colspan' => 2,
    ),
  );
  $result = db_query('SELECT qb.* FROM {quotes_blocks} qb ORDER BY qb.name');
  $rows = array();
  while ($block = db_fetch_object($result)) {
    $filters = array();
    if ($block->nid_filter) {
      $filters[] = t('node');
    }
    if ($block->rid_filter) {
      $filters[] = t('role');
    }
    if ($block->uid_filter) {
      $filters[] = t('user');
    }
    if ($block->tid_filter) {
      $filters[] = t('term');
    }
    $rows[] = array(
      $block->name,
      implode(', ', count($filters) ? $filters : array(
        t('none'),
      )),
      l(t('configure block'), "admin/build/block/configure/quotes/{$block->bid}"),
      l(t('delete block'), "admin/settings/quotes/delete/{$block->bid}"),
    );
  }
  $rows[] = array(
    drupal_render($form['name']),
    array(
      'data' => drupal_render($form['submit']),
      'colspan' => 3,
    ),
  );
  $output = drupal_render($form);
  if (count($rows)) {
    $output .= theme('table', $header, $rows);
  }
  else {
    $output .= theme('table', $header, array(
      array(
        array(
          'data' => t('No blocks are defined.'),
          'colspan' => 4,
        ),
      ),
    ));
  }
  return $output;
}

/**
 * Confirms the deletion a quote block.
 *
 * @param $bid
 *   The block ID of the block being deleted.
 *
 * @return
 *   A string containing the confirmation form displayed to the user.
 */
function _quotes_block_delete($bid) {
  $block = db_fetch_object(db_query('SELECT qb.name FROM {quotes_blocks} qb WHERE qb.bid = %d', $bid));
  $form = array();
  $form['bid'] = array(
    '#type' => 'value',
    '#value' => $bid,
  );
  $form['block_name'] = array(
    '#type' => 'value',
    '#value' => $block->name,
  );
  return confirm_form($form, t('Are you sure you want to delete the block %name?', array(
    '%name' => $block->name,
  )), 'admin/settings/quotes/blocks', t('This action cannot be undone.'), t('Delete'), t('Cancel'));
}

/**
 * Deletes the specified block.
 *
 * @param $form_id
 *   The string specifying the form ID of the form that was submitted.
 * @param $form_values
 *   The array specifying the form values.
 *
 * @result
 *   A string specifying the page to which the user should be
 *   redirected (admin/settings/quotes/blocks).
 */
function _quotes_block_delete_submit($form_id, $form_values) {
  db_query("DELETE FROM {quotes_blocks} WHERE bid = %d", $form_values['bid']);
  drupal_set_message(t('The block %name has been removed.', array(
    '%name' => $form_values['block_name'],
  )));
  cache_clear_all();
  return 'admin/settings/quotes/blocks';
}

/**
 * Produce an array of all authors.
 *
 * $return
 *   An associative array of authors in the quotes table.
 */
function quotes_get_authors() {
  $list = array();
  $unknown = -1;
  $result = db_query('SELECT qa.aid, qa.name, COUNT(q.nid) as count FROM {quotes_authors} qa LEFT JOIN {quotes} q USING(aid) GROUP BY qa.name ORDER BY qa.name');
  while ($row = db_fetch_array($result)) {
    if (empty($row['name'])) {
      $unknown = $row['aid'];
    }
    if ($row['count']) {
      $list[$row['aid']] = $row['name'];
    }
    else {

      //      drupal_set_message('There are no quotes for: '. filter_xss($row['name'], array()));
      db_query("DELETE FROM {quotes_authors} WHERE aid=%d", $row['aid']);
      watchdog('Quotes', t('Deleted aid=!aid (!name) because of no quotes.', array(
        '!aid' => $row['aid'],
        '!name' => drupal_substr(filter_xss($row['name'], array()), 0, 40),
      )));
    }
  }
  if ($unknown != -1) {
    $list[$unknown] = t('unspecified');
  }
  return $list;
}

/**
 * Produce an array of all citations.
 *
 * $return
 *   An associative array of citations in the quotes table.
 */
function quotes_get_citations() {
  $list = array();
  $result = db_query('SELECT DISTINCT(citation) FROM {quotes} ORDER BY citation');
  while ($row = db_fetch_array($result)) {
    $list[$row['citation']] = $row['citation'];
  }
  $list[' '] = t('unspecified');
  return $list;
}

/**
 * Implementation of hook_token_values().
 */
function quotes_token_values($type, $object = NULL) {
  $values = array();
  switch ($type) {
    case 'all':
    case 'node':
      $author = empty($object->quotes_author) ? 'unspecified' : $object->quotes_author;
      $values['quotes-author'] = decode_entities(check_plain($author));
      $values['quotes-author-raw'] = $author;
      $values['quotes-author-id'] = $object->quotes_aid;
      break;
  }
  return $values;
}

/**
 * Implementation of hook_token_list().
 */
function quotes_token_list($type = 'all') {
  $tokens = array();
  switch ($type) {
    case 'all':
    case 'node':
      $tokens['node']['quotes-author'] = t('The author of this quote.');
      $tokens['node']['quotes-author-raw'] = t('The author of this quote. WARNING - raw user input.)');
      $tokens['node']['quotes-author-id'] = t('The author id of this quote.)');
      break;
  }
  return $tokens;
}

Functions

Namesort descending Description
quotes_access Implementation of hook_access().
quotes_author Menu callback that selects quotes based on author.
quotes_author_form Form to select an author.
quotes_author_form_submit Handle submission of the form to select an author.
quotes_bios Bios maintenance page.
quotes_bios_submit
quotes_block Implementation of hook_block().
quotes_block_join_sql Returns the SQL join text necessary for the provided filter criteria.
quotes_block_where_sql Returns the SQL where text necessary for the provided filter criteria.
quotes_cron Implementation of hook_cron().
quotes_delete Implementation of hook_delete().
quotes_export Export function page.
quotes_form Implementation of hook_form().
quotes_form_alter Implementation of hook_form_alter().
quotes_get_authors Produce an array of all authors.
quotes_get_citations Produce an array of all citations.
quotes_get_quote Returns the node ID for either a random quote or the most recent quote based on the provided filter criteria.
quotes_help Implementation of hook_help().
quotes_insert Implementation of hook_insert(). This inserts the quote-specific information into the quotes tables and also handles the %id variable in the node title.
quotes_link Implementation of hook_link().
quotes_load Implementation of hook_load().
quotes_menu Implementation of hook_menu().
quotes_node_info Implementation of hook_node_info().
quotes_perm Implementation of hook_perm().
quotes_submit Implementation of hook_submit().
quotes_token_list Implementation of hook_token_list().
quotes_token_values Implementation of hook_token_values().
quotes_update Implementation of hook_update().
quotes_validate Implementation of hook_validate().
quotes_view Implementation of hook_view().
theme_quotes_page Themeable function that displays a page of quotes that may be restricted to a certain user.
theme_quotes_quote Themeable function that displays a single quote and optional author.
theme__quotes_blocks Renders the quote block list, including the "Add block" row.
_quotes_admin_settings Displays the admin settings form.
_quotes_autocomplete_author Function to provide autocomplete for author field.
_quotes_autocomplete_citation Function to provide autocomplete for citation field.
_quotes_blocks Displays a list of currently-defined quote blocks.
_quotes_blocks_submit Creates the new quote block.
_quotes_blocks_validate Validates that the new block name is valid.
_quotes_block_configuration_validate Validates that changes made on the block configuration screen are valid.
_quotes_block_configure Quotes block configuration.
_quotes_block_configure_save Quotes block configuration save.
_quotes_block_delete Confirms the deletion a quote block.
_quotes_block_delete_submit Deletes the specified block.
_quotes_escape_newlines Scans a string of text to remove new line chracters.
_quotes_feed_last Displays an RSS feed containing recent quotes of all users.
_quotes_feed_user Displays an RSS feed containing recent quotes of a given user.
_quotes_handle_author Helper function to fetch or insert an author.
_quotes_page Menu callback that calls theme_quotes_page().
_quotes_parse_import Parses and returns the quotes contained in the provided node body.
_quotes_trim Trim text to the admin-specified length.

Constants