You are here

quotes.module in Quotes 7

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

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>

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>
 */

/**
 * Current version release of the quotes module.
 */
$quotes_info = drupal_parse_info_file(drupal_get_path('module', 'quotes') . '/' . 'quotes.info');
define('QUOTES_VERSION', $quotes_info['version'] . ', release date ' . date(DATE_RFC822, $quotes_info['datestamp']));
define('QUOTES_BIOS_PATH', 'admin/config/content/quotes/bios');

// ********************************************************************/
// * Drupal Hooks
// ********************************************************************/
//

/**
 * Implements hook_permission().
 */
function quotes_permission() {
  return array(
    'access quotes' => array(
      'title' => t('access quotes'),
      'description' => t('Allow quotes display.'),
    ),
    'administer quotes' => array(
      'title' => t('administer quotes'),
      'description' => t('Perform administration tasks for quotes module.'),
    ),
    'create quotes' => array(
      'title' => t('create quotes'),
      'description' => t('Allow the creation of quotes.'),
    ),
    'import quotes' => array(
      'title' => t('import quotes'),
      'description' => t('Allow quotes to be imported to the quotes database.'),
    ),
    'edit own quotes' => array(
      'title' => t('edit own quotes'),
      'description' => t('Allow editing of own quotes.'),
    ),
    'promote quotes to block' => array(
      'title' => t('promote quotes to block'),
      'description' => t('Allow the promotion of quotes to a block.'),
    ),
  );
}

/**
 * Implements hook_node_access().
 */
function quotes_node_access($node, $op, $account) {
  $type = is_string($node) ? $node : $node->type;
  if ($type == 'quotes') {
    switch ($op) {
      case 'create':
        if (user_access('create quotes', $account) || user_access('import quotes', $account) || user_access('edit own quotes', $account)) {
          return NODE_ACCESS_ALLOW;
        }
        break;
      case 'update':
        if (user_access('edit own quotes', $account) && $account->uid == $node->uid) {
          return NODE_ACCESS_ALLOW;
        }
        break;
      case 'delete':
        if (user_access('edit own quotes', $account) && $account->uid == $node->uid) {
          return NODE_ACCESS_ALLOW;
        }
        if (user_access('administer quotes', $account)) {
          return NODE_ACCESS_ALLOW;
        }
        break;
      case 'view':
        if (user_access('access quotes', $account)) {
          return NODE_ACCESS_ALLOW;
        }
        break;
    }
  }

  // Returning nothing from this function would have the same effect.
  return NODE_ACCESS_IGNORE;
}

/**
 * Menu access callback.
 */
function _quotes_myquotes_access() {
  global $user;

  // This path is only allowed for authenticated users looking at their quotes
  // on thier account page.
  return $user->uid && variable_get('quotes_show_myquotes', TRUE);
}

/**
 * Menu access callback.
 */
function _quotes_recentquotes_access() {
  global $user;

  // This path is only allowed for authenticated users looking at their quotes.
  return $user->uid && variable_get('quotes_user_recent', FALSE);
}

/**
 * Implements hook_menu().
 */
function quotes_menu() {

  // Show tab on user account page.
  $items['user/%user/quotes'] = array(
    'title' => 'My Quotes',
    'page callback' => 'quotes_user_page',
    'page arguments' => array(
      1,
    ),
    'file' => 'quotes.user.inc',
    'access callback' => '_quotes_recentquotes_access',
    'type' => MENU_LOCAL_TASK,
    'weight' => 99,
  );

  // Menu callbacks.
  $items['quotes/%user/feed'] = array(
    'title' => 'RSS feed',
    'access arguments' => array(
      'access quotes',
    ),
    'page callback' => '_quotes_feed_user',
    'page arguments' => array(
      1,
    ),
    'type' => MENU_CALLBACK,
  );
  $items['quotes/feed'] = array(
    'title' => 'RSS feed',
    'access arguments' => array(
      'access quotes',
    ),
    'page callback' => '_quotes_feed_last',
    'type' => MENU_CALLBACK,
  );
  $items['quotes/autocomplete/author'] = array(
    'title' => 'Autocomplete Author Field',
    'access arguments' => array(
      'access quotes',
    ),
    'page callback' => '_quotes_autocomplete_author',
    'type' => MENU_CALLBACK,
  );
  $items['quotes/autocomplete/citation'] = array(
    'title' => 'Autocomplete Citation Field',
    'access arguments' => array(
      'access quotes',
    ),
    'page callback' => '_quotes_autocomplete_citation',
    'type' => MENU_CALLBACK,
  );

  // Quotes Selection by author menu entry.
  $items['quotes/byauthor'] = array(
    'title' => 'By author',
    'description' => 'Select quotes by author.',
    'page callback' => 'quotes_page',
    'page arguments' => array(
      1,
      "- FIND -",
    ),
    'access arguments' => array(
      'access quotes',
    ),
    'type' => MENU_NORMAL_ITEM,
  );

  // My quotes menu entry.
  $items['quotes/myquotes'] = array(
    'title' => 'My quotes',
    'page callback' => 'quotes_myquotes',
    'access callback' => '_quotes_myquotes_access',
    'type' => MENU_NORMAL_ITEM,
  );

  // Quotes Selection by author menu entry.
  $items['quotes/%'] = array(
    'title' => 'Accounts quotes page',
    'page callback' => 'quotes_page',
    'page arguments' => array(
      1,
    ),
    'access arguments' => array(
      'access quotes',
    ),
    'type' => MENU_CALLBACK,
  );

  // Primary menu entry.
  $items['quotes'] = array(
    'title' => 'Quotes',
    'access arguments' => array(
      'access quotes',
    ),
    'page callback' => 'quotes_page',
    'type' => MENU_NORMAL_ITEM,
  );

  // Begin quotes page tabs.
  $quick_nav = variable_get('quotes_quick_nav', TRUE);

  // Add new quote.
  $items['quotes/add'] = array(
    'title' => 'Add a new quote',
    'page callback' => '_quotes_add',
    'page arguments' => array(
      1,
    ),
    'access arguments' => array(
      'create quotes',
    ),
    'type' => $quick_nav ? MENU_LOCAL_TASK : MENU_CALLBACK,
  );
  $items['quotes/author/%'] = array(
    'title' => 'By author',
    'description' => 'Select quotes by author.',
    'page callback' => 'quotes_page',
    'page arguments' => array(
      1,
      2,
    ),
    'access arguments' => array(
      'access quotes',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['quotes/author'] = array(
    'title' => 'By author',
    'description' => 'Select quotes by author.',
    'page callback' => 'quotes_page',
    'page arguments' => array(
      1,
      "- FIND -",
    ),
    'access arguments' => array(
      'access quotes',
    ),
    'type' => $quick_nav ? MENU_LOCAL_TASK : MENU_CALLBACK,
  );

  // Import quotes.
  $items['quotes/import'] = array(
    'title' => 'Import quotes',
    'page callback' => '_quotes_add',
    'page arguments' => array(
      1,
    ),
    'access arguments' => array(
      'import quotes',
    ),
    'type' => $quick_nav ? MENU_LOCAL_TASK : MENU_CALLBACK,
  );

  // Admin settings for the site.
  $items['quotes/settings'] = array(
    'title' => 'Settings',
    'description' => 'Configure Quotes module options and blocks.',
    'page callback' => '_quotes_settings',
    'access arguments' => array(
      'administer quotes',
    ),
    'page arguments' => array(
      1,
    ),
    'type' => $quick_nav ? MENU_LOCAL_TASK : MENU_CALLBACK,
  );

  // Admin configuration settings for quotes.
  $items['admin/config/content/quotes'] = array(
    'access arguments' => array(
      'administer quotes',
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'quotes_admin_settings',
    ),
    'title' => 'Quotes module',
    'file' => 'quotes.admin.inc',
    'description' => 'Configure Quotes module options and blocks.',
  );
  $items['admin/config/content/quotes/general'] = array(
    'title' => 'General',
    'description' => 'Quotes configuration settings.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'quotes_admin_settings',
    ),
    'access arguments' => array(
      'administer quotes',
    ),
    'file' => 'quotes.admin.inc',
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/config/content/quotes/blocks'] = array(
    'title' => 'Configure blocks',
    'description' => 'Quotes blocks Configuration.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'quotes_blocks_settings',
    ),
    'access arguments' => array(
      'administer quotes',
    ),
    'file' => 'quotes.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 1,
  );
  $items['admin/config/content/quotes/export'] = array(
    'title' => 'Export',
    'description' => 'Export your Quotes',
    'page callback' => 'drupal_get_form',
    'access arguments' => array(
      'administer quotes',
    ),
    'page arguments' => array(
      'quotes_export',
    ),
    'file' => 'quotes.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 3,
  );
  $items[QUOTES_BIOS_PATH] = array(
    'title' => 'Bios',
    'description' => 'Quotes author bios',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'quotes_bios',
    ),
    'access arguments' => array(
      'administer quotes',
    ),
    'file' => 'quotes.admin.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => 2,
  );
  $items['admin/config/content/quotes/delete'] = array(
    'title' => 'Delete quote block',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      '_quotes_block_delete',
    ),
    'access arguments' => array(
      'administer quotes',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'quotes.admin.inc',
  );
  return $items;
}

/**
 * Add a quote.
 */
function _quotes_add() {
  drupal_goto('node/add/quotes');
}

/**
 * Menu call to the quotes settings page.
 */
function _quotes_settings() {
  drupal_goto('admin/config/quotes');
}

/**
 * Implements hook_menu_alter().
 */
function quotes_menu_alter(&$callbacks) {

  // Get what the admin wants to call it.
  // Note: menu_get_item('quotes') returns the original, not the overridden.
  $menu_title = db_query("SELECT link_title FROM {menu_links} WHERE router_path = 'quotes'")
    ->fetchField();
  if (empty($menu_title)) {
    $menu_title = 'Quotes';
  }
  $callbacks['quotes']['title'] = $menu_title;
  $callbacks['quotes/%']['title'] = $menu_title . ' page';
  $callbacks['user/%user/quotes']['title'] = $menu_title;
}

/**
 * Implements hook_init().
 */
function quotes_init() {
  drupal_add_css(drupal_get_path('module', 'quotes') . '/quotes.css');
}

/**
 * Implements hook_help().
 */
function quotes_help($path, $args) {
  $output = NULL;
  $fieldset = array(
    '#title' => t('Notes for importing'),
    '#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: blue;">author</span>
        <span style="color: red;">&lt;tab&gt;</span>
        <span style="color: blue;">citation</span>
        </em>. The author and citation are optional. To import quotes, authors, or citations with more than one line, escape the embedded newlines with a backslash.
        <p>Examples:</p>
        <pre style="font-size: .8em;">
          <span style="color: blue;">Single-line quote.</span><em style="color: red;">&lt;tab&gt;</em><span style="color: blue;">Author</span><em style="color: red;">&lt;tab&gt;</em><span style="color: blue;">Citation</span>
          <span style="color: blue;">Quote without citation.</span><em style="color: red;">&lt;tab&gt;</em><span style="color: blue;">Author</span><em style="color: red;">&lt;tab&gt;</em>
          <span style="color: blue;">Quote without author or citation.</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: blue;">Author line 1\\
          Author line 2</span>
          <span style="color: blue;">Another quote.<em style="color: red;">&lt;tab&gt;</em><span style="color: blue;">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> and will extract a citation
      when there is a line of the form <em>--Citation</em> respectively with any
      amount of leading whitespace. If a citation is used without an author
      then the author line has to be included or the citation will become the author.
        <p>Examples:</p>
        <pre style="font-size: .8;">
        <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: blue;"> Author</span>
        <em style="color: red;">%</em>
        <span style="color: blue;">Fortune with author and citation.</span>
        --<span style="color: blue;"> Author</span>
        --<span style="color: blue;"> Citation</span>
        <em style="color: red;">%</em>
        <span style="color: blue;">Fortune without author and citation.</span>
        --<span style="color: blue;"> </span>
        --<span style="color: blue;"> Citation</span>
        <em style="color: red;">%</em>
      <span style="color: blue;">Multi-line fortune: line 1...
      ...line 2.</span>
      -- <span style="color: blue;"> 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>'),
    '#attributes' => array(
      'class' => array(
        'collapsed',
        'collapsible',
      ),
    ),
    '#children' => '',
  );
  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':
      if (arg(3) == '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(
          'element' => $fieldset,
        ));
      }
      else {
        $output .= t('Use the form below to enter a single quote.');
        if (user_access('import quotes')) {
          $output .= ' ' . 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'),
          ));
        }
      }
      return $output;
    case 'admin/config/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/people/permissions'),
      ));
      $text .= '<p>Version: ' . QUOTES_VERSION . '</p>';
      return $text;
    case 'admin/config/quotes/blocks':
      $text = 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 the !block administration page and as the default title when the block is displayed.', array(
        '!block administration page' => l(t('block administration page'), 'admin/structure/block'),
      ));
      return $text;
    case 'admin/config/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.');
  }
}

/**
 * Implements hook_node_info().
 */
function quotes_node_info() {
  return array(
    'quotes' => array(
      'name' => t('Quotes'),
      'base' => '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.'),
      'has_title' => TRUE,
    ),
  );
}

/**
 * Implements hook_load().
 */
function quotes_load($nodes) {
  foreach ($nodes as $node) {

    // Check node access then continue if allowed or skip to next node.
    if (node_access('view', $node)) {
      try {
        $obj = db_select('quotes', 'q');
        $alias = $obj
          ->join('quotes_authors', 'a', 'q.aid = a.aid');
        $f_aa = $obj
          ->addField('a', 'aid', 'quotes_aid');
        $f_an = $obj
          ->addField('a', 'name', 'quotes_author');
        $f_ab = $obj
          ->addField('a', 'bio', 'quotes_bio');
        $f_qc = $obj
          ->addField('q', 'citation', 'quotes_citation');
        $f_qp = $obj
          ->addField('q', 'promote', 'quotes_promote');
        $obj1 = $obj
          ->condition('q.vid', $node->vid)
          ->execute()
          ->fetchObject();
      } catch (Exception $e) {
        drupal_set_message(t('db_insert failed. Message = %message, query= %query', array(
          '%message' => $e
            ->getMessage(),
          '%query' => $e->query_string,
        )), 'error');
      }
      if ($obj1) {
        foreach ($obj1 as $property => $value) {
          $nodes[$node->nid]->{$property} = $value;
        }
      }
    }
  }
}

/**
 * Implements hook_insert().
 */
function quotes_insert($node) {
  quotes_import($node);
}

/**
 * Inserts quotes data into db tables and handles %id variable in quotes title.
 *
 * @param array $node
 *   As an import $node->quotes_import_format defines what type of import is
 *   required, either tab delimited or fortune format. The body of the node is
 *   then parsed by calling _quotes_pasre_import which splits the body as needed
 *   returning the data to import.
 *
 *   If called from quotes_insert() then this function serves to save a single
 *   quote to the database.
 *
 *   This function also renames the title if set to %id to the new nodes nid
 *   value.
 *
 *   Upon completion, user is returned to the quotes page on imports.
 */
function quotes_import($node) {

  // Is it a single node or an import?
  switch ($node->quotes_import_format) {
    case 'text':
    case 'fortune':
      $quotes_found = _quotes_parse_import($node, FALSE);
      foreach ($quotes_found as $count => $quote) {

        // Create a new node object.
        $newnode = (object) NULL;
        $newnode->type = "quotes";
        $newnode->title = $node->title;
        $newnode->uid = $node->uid;
        $newnode->created = strtotime("now");
        $newnode->changed = strtotime("now");
        $newnode->status = 1;
        $newnode->comment = 1;
        $newnode->promote = 1;
        $newnode->moderate = 0;
        $newnode->sticky = 0;
        $newnode->is_new = 1;
        $newnode->quotes_import_format = 'single';
        $newnode->body[$node->language][0]['value'] = filter_xss($quote->body);
        if (!isset($newnode->body[$node->language][0]['safe_value'])) {
          $newnode->body[$node->language][0]['safe_value'] = check_markup($quote->body);
        }

        // Create the teaser if required.
        $teaser = text_summary($quote->body, isset($quote->format) ? $quote->format : filter_format_default());

        // We do not want a teaser or summary in a quote usually.
        // $node->body[$node->language][0]['summary'] = $teaser;
        $newnode->body[$node->language][0]['format'] = $quote->format;
        $newnode->quotes_author = $quote->quotes_author;
        $newnode->quotes_citation = $quote->quotes_citation;
        if (isset($node->quotes_promote)) {
          $newnode->quotes_promote = $node->quotes_promote;
        }
        else {
          $newnode->quotes_promote = 0;
        }

        // Write the imported quote.
        node_save($newnode);
        $node == $newnode;
        continue;
      }

      // Done with the import, return directly to quotes page.
      drupal_set_message(t('@count quotes imported.', array(
        '@count' => $count + 1,
      )));
      drupal_goto('quotes');
      break;
    case 'single':

      // Save the author, citation, and promote values into our table.
      $aid = _quotes_handle_author($node->quotes_author);
      db_insert('quotes')
        ->fields(array(
        'nid' => (int) $node->nid,
        'vid' => (int) $node->vid,
        'aid' => (int) $aid,
        'citation' => $node->quotes_citation,
        'promote' => (int) $node->quotes_promote,
      ))
        ->execute();

      // Replace %id  title variable with nid.
      if (strpos($node->title, '%id') !== FALSE) {
        $node->title = str_replace('%id', $node->nid, $node->title);
        db_update('node')
          ->fields(array(
          'title' => $node->title,
        ))
          ->condition('vid', $node->vid)
          ->execute();
        db_update('node_revision')
          ->fields(array(
          'title' => $node->title,
        ))
          ->condition('vid', $node->vid)
          ->execute();
      }
  }
}

/**
 * Helper function to fetch or insert an author.
 *
 * @param string $author
 *   The text of the author's name.
 */
function _quotes_handle_author($author = NULL) {
  if (empty($author)) {
    return FALSE;
  }
  $result = db_select('quotes_authors', 'a')
    ->fields('a', array(
    'aid',
  ))
    ->condition('a.name', $author)
    ->execute();
  if ($result
    ->rowCount()) {
    return $result
      ->fetchField();
  }
  $aid = db_insert('quotes_authors')
    ->fields(array(
    'name' => $author,
  ))
    ->execute();
  if ($aid === FALSE) {
    drupal_set_message(t('Quotes: insert author failed.'), 'error');
  }
  return $aid;
}

/**
 * Implements hook_update().
 */
function quotes_update($node) {
  $aid = _quotes_handle_author($node->quotes_author);
  if ($node->revision) {
    quotes_insert($node);
  }
  else {
    db_update('quotes')
      ->fields(array(
      'aid' => 0 + $aid,
      'citation' => $node->quotes_citation,
      'promote' => (int) $node->quotes_promote,
    ))
      ->condition('nid', $node->nid)
      ->condition('vid', $node->vid)
      ->execute();
  }
}

/**
 * Implements hook_delete().
 */
function quotes_delete($node) {

  // Check if author is present, if so continue.
  if (isset($node->quotes_author)) {
    $aid = _quotes_handle_author();

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

  // Now delete the quote from the quotes table.
  db_query('DELETE FROM {quotes} WHERE nid = :q_nid', array(
    ':q_nid' => $node->nid,
  ));
}

/**
 * Implements hook_form().
 */
function quotes_form($node, $form_state) {
  $form = array(
    'quotes_data' => array(),
  );
  $form['title'] = array(
    '#type' => 'textfield',
    '#title' => t('Title'),
    '#required' => FALSE,
    '#default_value' => isset($node->title) ? $node->title : '',
    '#description' => t("Enter the title for the quote(s). If you include the variable %id, it will be replaced by the new quote's ID."),
  );

  // Set default value, php 5.3 does not like undefined properties, .
  $quotes_import_format = 'single';
  if (user_access('import quotes')) {
    $form['quotes_import_format'] = array(
      '#type' => 'radios',
      '#title' => t('Format'),
      '#required' => TRUE,
      '#default_value' => $quotes_import_format,
      '#options' => array(
        'single' => t('Single quote.'),
        'text' => t('Import tab-separated text.'),
        'fortune' => t('Import Fortune file.'),
      ),
    );
  }
  else {
    $form['quotes_import_format'] = array(
      '#type' => 'value',
      '#value' => 'single',
    );
  }
  if (user_access('import quotes')) {
    $form['quotes']['body_field']['#description'] = t('Enter the text of the quote or the group of quotes to be imported. It will be filtered according to the input format. Note: The "Split summary" button cannot be used when importing a group of quotes.');
    $no_import = ' ' . t('This should not be used when importing.');
  }
  else {
    $form['quotes']['body_field']['#description'] = t('Enter the text of the quote. It will be filtered according to the input format.');
    $no_import = NULL;
  }

  // Push input format down below weight.
  $form['quotes']['format']['#weight'] = 1;
  $author = '';
  if (isset($node->quotes_author)) {
    $author = $node->quotes_author;
  }
  $form['quotes']['quotes_author'] = array(
    '#type' => 'textfield',
    '#title' => t('Author'),
    '#autocomplete_path' => 'quotes/autocomplete/author',
    '#rows' => 1,
    '#maxlength' => 254,
    '#default_value' => $author,
    '#description' => check_plain(t('This is who is credited for the quotation.') . $no_import),
  );
  $citation = '';
  if (isset($node->quotes_citation)) {
    $citation = $node->quotes_citation;
  }
  $form['quotes']['quotes_citation'] = array(
    '#type' => 'textfield',
    '#title' => t('Citation'),
    '#autocomplete_path' => 'quotes/autocomplete/citation',
    '#rows' => 1,
    '#maxlength' => 1023,
    '#default_value' => $citation,
    '#description' => check_plain(t('This is the source (book, magazine, etc.) of the quote.') . $no_import),
  );
  if (user_access('promote quotes to block')) {
    $form['quotes']['quotes_promote'] = array(
      '#type' => 'checkbox',
      '#title' => t('Display in quote blocks'),
      '#default_value' => isset($node->quotes_promote) ? $node->quotes_promote : 1,
      '#description' => t('This option allows you to decide whether this quote will be displayable in the blocks.'),
    );
  }
  return $form;
}

/**
 * Implements hook_view().
 */
function quotes_view($node, $view_mode) {
  global $user;

  // Breadcrumb navigation.
  if (node_is_page($node)) {
    $build = array();
    $breadcrumb = array();
    $breadcrumb[] = l(t('Home'), variable_get('site_frontpage', 'node'));

    // Get the menu title.
    $menu_info = menu_get_item('quotes');
    $menu_title = $menu_info['title'];
    $breadcrumb[] = l($menu_title, 'quotes');
    $username = $user->uid ? $user->name : variable_get('anonymous', t('Anonymous'));
    $breadcrumb[] = l(t("!name's !menu", array(
      '!menu' => $menu_title,
      '!name' => $username,
    )), "quotes/{$node->uid}");
    drupal_set_breadcrumb($breadcrumb);
  }
  $format = variable_get('quotes_format', filter_default_format());
  $variables['node'] = $node;

  // Format to use for author and citation.
  $variables['format'] = $format;

  // Are we in a quotes_block.
  $in_block = isset($node->in_block) && $node->in_block;

  // We do not want the title in the quotes block.
  if ($in_block && isset($node->show_titles) && $node->show_titles == 0) {
    $node->title = '';
  }
  if (!$in_block && empty($node->title)) {
    quotes_form_alter($form, $formstate, $form_id = NULL);
  }

  // Add author if present.
  theme_quotes_author($variables);
  $node = $variables['node'];

  // Add citation if present.
  $variables['node'] = $node;
  theme_quotes_citation($variables);
  $node = $variables['node'];

  // Now the links.
  if ($node->type == 'quotes') {

    // Some links are not done in blocks.
    // Get the menu title.
    $menu_info = menu_get_item('quotes');
    $menu_title = $menu_info['title'];
    if (!(arg(0) == 'quotes' && arg(1) == $node->uid)) {
      $name = $node->uid ? $node->name : variable_get('anonymous', t('Anonymous'));

      // Note: links already get check_plain, so the text is safe.
      if (!$in_block && variable_get('quotes_showlink', TRUE)) {
        if ($node->uid != $user->uid) {
          $links['quotes_usernames_quotes'] = array(
            'title' => t("!name's !menu", array(
              '!menu' => $menu_title,
              '!name' => $name,
            )),
            'href' => "quotes/{$node->uid}",
            'attributes' => array(
              'title' => t("View !name's !menu.", array(
                '!menu' => $menu_title,
                '!name' => $name,
              )),
            ),
          );
          $node->content['links']['quotes'] = array(
            '#theme' => 'links__node__quotes',
            '#links' => $links,
            '#attributes' => array(
              'class' => array(
                'links',
                'inline',
              ),
            ),
          );
        }
      }
      if (!$in_block && 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 !menu', array(
              '!menu' => drupal_strtolower($menu_title),
            )),
            'href' => 'node/' . $node->nid . '/edit',
            'attributes' => array(
              'title' => t('Edit this !menu', array(
                '!menu' => drupal_strtolower($menu_title),
              )),
            ),
          );
          $node->content['links']['quotes'] = array(
            '#theme' => 'links__node__quotes',
            '#links' => $links,
            '#attributes' => array(
              'class' => array(
                'links',
                'inline',
              ),
            ),
          );
        }
      }
    }
  }
  return $node;
}

/**
 * Implements hook_node_view().
 */
function quotes_node_view($node, $view_mode) {
  global $user;

  // Are we in a quotes_block.
  $in_block = isset($node->in_block) && $node->in_block;
  if ($node->type == 'quotes' && $view_mode == 'full') {
    $format = variable_get('quotes_format', filter_default_format());
    $variables['node'] = $node;

    // Format to use for author and citation.
    $variables['format'] = $format;

    // Add author's bio to top of page if enabled and present.
    if (variable_get('quotes_author_bio', FALSE) >= 1 && !empty($node->quotes_bio)) {
      if (!$in_block && variable_get('quotes_bio_set', FALSE) == 0) {
        $node->content['quotes_bio'] = array(
          '#prefix' => '<div class="quotes-header-bio">',
          '#suffix' => '</div>',
          '#markup' => check_markup($node->quotes_bio, $format, $node->language, FALSE),
        );
        variable_set('quotes_bio_set', TRUE);
      }
    }
    if (arg(0) == 'quotes' and arg(1) == 'author') {
      $node->title = '';
    }

    // Plain text title for block.
    if ($in_block and $node->show_titles == 2) {

      // @todo - Needs template to do this or to use
      // an extra field to overwrite node title. Interesting!
    }

    // Add Author if present.
    theme_quotes_author($variables);
    $node = $variables['node'];

    // Add citation if present.
    $variables['node'] = $node;
    theme_quotes_citation($variables);
    $node = $variables['node'];

    // Now the links.
    if (arg(0) == 'quotes' && arg(1) == 'author') {

      // Only show this link on authors page.
      if (!$in_block && variable_get('quotes_edit_link', TRUE)) {
        if (user_access('edit own quotes') && $node->uid == $user->uid || user_access('administer quotes')) {
          $links['quotes_editlink'] = array(
            'title' => check_plain('Edit'),
            'href' => 'node/' . $node->nid . '/edit',
            'attributes' => array(
              'class' => array(
                'quotes-edit-link',
              ),
              'title' => check_plain('Edit'),
            ),
          );
          $node->content['links']['quotes'] = array(
            '#theme' => 'links__node__quotes',
            '#links' => $links,
            '#attributes' => array(
              'class' => array(
                'links',
                'inline',
              ),
            ),
          );
        }
      }
    }
    if ($in_block) {

      // Are we changing the more link text.
      if ($in_block && $node->more_text) {
        $links['view_link'] = array(
          'title' => check_plain($node->more_text),
          'href' => 'quotes',
          'attributes' => array(
            'class' => array(
              'quotes-more-link',
            ),
            'title' => check_plain($node->more_text),
          ),
        );
        $node->content['links']['quotes'] = array(
          '#theme' => 'links__node__quotes',
          '#links' => $links,
          '#attributes' => array(
            'class' => array(
              'links',
              'inline',
            ),
          ),
        );
      }
    }

    // Is the comments installed and enambled, if so do we wish to display
    // a link to access them?
    if (module_exists('comment') && $in_block && $node->comment == COMMENT_NODE_OPEN && !empty($node->view_text)) {
      $links['view_link'] = array(
        'title' => $node->view_text,
        'href' => 'node/' . $node->nid,
        'attributes' => array(
          'class' => 'quotes-view-link',
        ),
      );
      $node->content['links']['quotes'] = array(
        '#theme' => 'links__node__quotes',
        '#links' => $links,
        '#attributes' => array(
          'class' => array(
            'links',
            'inline',
          ),
        ),
      );
    }
  }

  // RSS Feeds.
  if ($view_mode != 'rss') {
    if ($node->type == 'quotes' && !(arg(0) == 'quotes' && arg(1) == $node->uid)) {
      $name = $node->uid ? $node->name : variable_get('anonymous', t('Anonymous'));
      $links = array();
      if (variable_get('quotes_showlink', TRUE)) {
        if ($node->uid != $user->uid) {

          // Get the menu title.
          $menu_info = menu_get_item('quotes');
          $menu_title = $menu_info['title'];
          $links['quotes_usernames_quotes'] = array(
            'title' => t("!username's quotes", array(
              '!username' => format_username($node),
            )),
            'href' => "quotes/{$node->uid}",
            'attributes' => array(
              'title' => t("Read !username's latest quotes entries.", array(
                '!username' => format_username($node),
              )),
            ),
          );
          $node->content['links']['quotes'] = array(
            '#theme' => 'links__node__quotes',
            '#links' => $links,
            '#attributes' => array(
              'class' => array(
                'links',
                'inline',
              ),
            ),
          );
        }
      }
    }
  }
}

/**
 * Implements hook_block_info().
 */
function quotes_block_info() {
  $blocks = array();
  $result = db_query('SELECT qb.bid, qb.name, qb.block_type FROM {quotes_blocks} qb');
  foreach ($result as $block) {
    $blocks[$block->bid] = array(
      'info' => t('Quotes') . ': ' . $block->name,
      'cache' => DRUPAL_NO_CACHE,
    );
  }
  return $blocks;
}

/**
 * Implements hook_block_view().
 */
function quotes_block_view($delta) {
  $quotes = '';

  // TODO Rename block deltas (e.g., delta-0) to readable strings.
  $block = db_select('quotes_blocks', 'qb')
    ->fields('qb')
    ->condition('qb.bid', $delta)
    ->execute()
    ->fetchAssoc();
  if (!$block) {
    return NULL;
  }
  if ($block['cron_interval'] > 0) {
    if (!$block['vid'] || $block['cron_last'] + $block['cron_interval'] * $block['cron_step'] < REQUEST_TIME) {
      $block['vid'] = quotes_get_quote($block, TRUE, 1);
      db_update('quotes_blocks')
        ->fields(array(
        'vid' => $block['vid'][0],
        'cron_last' => REQUEST_TIME,
      ))
        ->condition('bid', $delta)
        ->execute();
      cache_clear_all();
    }
    else {
      $block['vid'] = array(
        $block['vid'],
      );
    }
  }
  else {
    $block['vid'] = quotes_get_quote($block, TRUE, $block['count']);
  }
  if (!$block['vid']) {
    return NULL;
  }
  foreach ($block['vid'] as $nid) {
    $nodes[] = node_load($nid);
  }
  $variables['block'] = $block;
  $variables['nodes'] = $nodes;
  theme_quotes_block($variables);
  return $variables['block'];
}

/**
 * Implements hook_preprocess_node().
 */
function quotes_preprocess_node($variables) {
  $node = $variables['node'];

  // We only do this for our content and only on the links page.
  if ($variables['type'] == 'quotes') {

    // Are we processing the view in a block?
    if (isset($node->in_block) && $node->in_block) {
      unset($variables['submitted']);

      // Do we want the citation?
      if ($node->show_citation == 0 && isset($node->quotes_citation)) {
        $node->quotes_citation == NULL;
      }

      // Do we want titles?
      switch ($node->show_titles) {
        case 1:

          // Link to node.
          $variables['node_url'] = '/node/' . $variables['nid'];
          break;
        case 2:

          // Plain text.
          unset($variables['node_url']);
          $variables['title'] = check_plain($node->title);
          break;
      }
    }
  }
}

/**
 * Quotes multi-block blocking.
 */
function quotes_block_mb_blocked() {
  return 'quotes';
}

/**
 * Implements hook_block_configure().
 */
function quotes_block_configure($delta) {
  return _quotes_block_configure($delta);
}

/**
 * Implements hook_block_save().
 */
function quotes_block_save($delta, $edit) {
  quotes_block_configure_save($delta, $edit);
}

/**
 * Quotes block configuration.
 *
 * @param int $delta
 *   The block ID that we are working with.
 *
 * @return array
 *   returns the form data for further processing.
 */
function _quotes_block_configure($delta) {
  global $_quotes_subs;
  drupal_add_js(drupal_get_path('module', 'quotes') . '/js/quotes.js');
  $none_opt = '- ' . t('none') . ' -';
  $block = db_query("SELECT qb.* FROM {quotes_blocks} qb WHERE bid = :bid", array(
    ':bid' => $delta,
  ))
    ->fetchObject();
  $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(TRUE);

  // 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;
  }
  $roles[-1] = $none_opt;
  asort($roles);

  // Get users.
  $users = array(
    -1 => $none_opt,
  );
  $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,
  )) . ")");
  foreach ($result as $row) {
    $users[$row->uid] = $row->uid ? $row->name : variable_get('anonymous', t('Anonymous'));
  }
  asort($users);
  $form = array();
  if ($delta) {
    $form['bid'] = array(
      '#type' => 'value',
      '#value' => $delta,
    );
  }
  $form['quotes'] = array(
    '#type' => 'fieldset',
    '#title' => 'Quotes specific settings',
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
    '#prefix' => '<div id="quotes-block-settings">',
    '#suffix' => '</div>',
  );
  $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>',
  );
  $_quotes_subs = array(
    '%' . t('interval') => NULL,
    '%' . t('bid') => NULL,
    '%' . t('title') => NULL,
    '%' . t('nid') => NULL,
    '%' . t('user') => NULL,
  );
  $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/structure/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' => isset($block->show_citation) ? $block->show_citation : FALSE,
    '#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. It is suggested that no more than 5 quotes per block be used when the block type is set to "Random".'),
  );
  $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.<br />This is only used when comments are enabled!'),
  );
  $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.'),
  );
  $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.'),
  );
  $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'),
      '#default_value' => $block->aid_filter ? $block->aid_filter : '',
      '#description' => t('There are no authors.  To filter by authors, there must be at least one quote with an attributed author'),
    );
  }
  if ($block->rid_filter == array(
    '',
  )) {
    $block->rid_filter = NULL;
  }
  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'),
      '#default_value' => $block->rid_filter ? $block->rid_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('administer permissions page'), 'admin/user/permissions'),
      )),
    );
  }
  if ($block->uid_filter == array(
    '',
  )) {
    $block->uid_filter = NULL;
  }
  $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 ($block->tid_filter == array(
    '',
  )) {
    $block->tid_filter = NULL;
  }
  $taxo_opts = array();
  $taxo_opts = array(
    -1 => $none_opt,
  );

  // Get only the quotes module vocabulary.
  $names = db_query('SELECT vid FROM {taxonomy_vocabulary} WHERE name = :name', array(
    ':name' => 'quotes',
  ))
    ->fetchField();

  // If there isn't a vocabulary skip getting its terms.
  if ($names) {
    $terms = taxonomy_get_tree($names, $parent = 0, $max_depth = NULL, $load_entities = FALSE);
    foreach ($terms as $term) {
      $taxo_opts[$term->tid] = $term->name;
    }
    asort($taxo_opts);
    $form['quotes']['filters']['tid_filter'] = array(
      '#type' => 'select',
      '#title' => t('Category filter'),
      '#multiple' => TRUE,
      '#default_value' => $block->tid_filter,
      '#options' => $taxo_opts,
      '#description' => t('To restrict this block to display only quotes in specific categories, select the categories here.'),
    );
  }
  else {
    $form['quotes']['filters']['tid_filter'] = array(
      '#type' => 'item',
      '#title' => t('Category filter'),
      '#default_value' => $block->tid_filter ? $block->tid_filter : '',
      '#description' => t('The taxanomy vocabulary has not been set up. Please setup a vocabulary item at the !taxvocabadd&nbsp;
                           (it is necessay to set the machine_name to <em>quotes</em> for this vocabulary item.<br />
                           Once the vocabulaby has been created then add any terms (Categories) to your newly created vocabulary item.
                           These terms will then be available to select in the quotes block configuration area.', array(
        '!taxvocabadd' => l(t('taxonomy administration page'), 'admin/structure/taxonomy'),
      )),
    );
  }
  $form['quotes']['cron'] = array(
    '#type' => 'fieldset',
    '#title' => t('Update options'),
    '#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(
      1 => t('seconds'),
      60 => t('minutes'),
      3600 => t('hours'),
      86400 => t('days'),
      604800 => 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.
 *
 * We save our data to the quotes_block tables as in D6.
 *
 * @param int $delta
 *   The blocks ID value we are saving.
 */
function quotes_block_configure_save($delta, $edit) {
  $vals = array(
    $edit['name'],
    $edit['block_type'],
    preg_replace('<[,\\s]+>', ',', trim($edit['nid_filter'])),
    $edit['aid_filter'] == array(
      '0',
    ) ? '' : implode(',', (array) $edit['aid_filter']),
    $edit['rid_filter'] == array(
      -1 => '-1',
    ) ? '' : implode(',', (array) $edit['rid_filter']),
    $edit['uid_filter'] == array(
      -1 => '-1',
    ) ? '' : implode(',', (array) $edit['uid_filter']),
    // I'm not sure why it returns 0, but it works for me.
    $edit['tid_filter'] == array(
      -1 => '-1',
    ) ? '' : implode(',', (array) $edit['tid_filter']),
    $edit['cron_interval'] ? $edit['cron_interval'] : 0,
    $edit['cron_step'],
    $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['max_length'],
    // Frequency is only for random blocks.
    $edit['block_type'] == 0 ? $edit['rand_freq'] : 100,
    $delta,
  );
  db_update('quotes_blocks')
    ->fields(array(
    'name' => $vals[0],
    'block_type' => $vals[1],
    'nid_filter' => $vals[2],
    'aid_filter' => $vals[3],
    'rid_filter' => $vals[4],
    'uid_filter' => $vals[5],
    'tid_filter' => $vals[6],
    'cron_interval' => $vals[7],
    'cron_step' => $vals[8],
    'count' => $vals[9],
    'show_titles' => $vals[10],
    'show_citation' => $vals[11],
    'more_text' => $vals[12],
    'view_text' => $vals[13],
    'max_length' => $vals[14],
    'rand_freq' => $vals[15],
  ))
    ->condition('bid', $delta)
    ->execute();
}

/**
 * Implements hook_form_alter().
 */
function quotes_form_alter(&$form, &$form_state, $form_id) {
  if ($form_id == 'block_admin_configure' && $form['module']['#value'] == 'quotes') {
    $form['#validate'][] = '_quotes_block_configuration_validate';
  }
}

/**
 * Form submit presave handler for importing quotes.
 */
function quotes_node_presave($node) {
  if ($node->type === 'quotes' && ($node->quotes_import_format == 'text' || $node->quotes_import_format == 'fortune')) {
    if ($node->op == 'Save') {
      quotes_import($node);
    }
  }
}

/**
 * Validates that changes made on the block configuration screen are valid.
 *
 * @param array $form
 *   The form that was submitted.
 *
 * @param array $form_state
 *   The array specifying the form values.
 */
function _quotes_block_configuration_validate($form, &$form_state) {
  if (trim($form_state['values']['nid_filter']) && !preg_match('<^(\\d+[,\\s]*)+$>', trim($form_state['values']['nid_filter']))) {
    form_set_error('nid_filter', t('Please enter valid node IDs.'));
  }
  $interval = $form_state['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.'));
  }
}

/**
 * Implements hook_cron().
 */
function quotes_cron() {
  $result = db_query("SELECT qb.* FROM {quotes_blocks} qb INNER JOIN {block} 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)) < :time)", array(
    ':time' => REQUEST_TIME,
  ));
  if ($result) {
    for ($updated = FALSE; $block = $result
      ->fetchAssoc(); $updated = TRUE) {
      $quotes = quotes_get_quote($block, TRUE, 1);
      db_update('quotes_blocks')
        ->fields(array(
        'vid' => $quotes[0],
        'cron_last' => REQUEST_TIME,
      ))
        ->condition('bid', $block['bid'])
        ->execute();
    }
    if ($updated) {
      cache_clear_all();
    }
  }
}

/**
 * Implements hook_theme().
 */
function quotes_theme() {
  $theme_hooks = array(
    'quotes_user_form' => array(
      'render element' => 'form',
      'file' => 'quotes.user.inc',
    ),
    'quotes_page' => array(
      'variables' => array(
        'uid' => NULL,
        'account' => NULL,
        'user' => NULL,
      ),
      'template' => 'quotes',
    ),
    'quotes_author' => array(
      'variables' => array(
        'node' => NULL,
        'format' => NULL,
      ),
    ),
    'quotes_citation' => array(
      'variables' => array(
        'node' => NULL,
        'format' => NULL,
      ),
    ),
    'quotes_block' => array(
      'variables' => array(
        'block' => NULL,
        'nodes' => NULL,
      ),
      'template' => 'quotes-block',
    ),
  );

  // The theme system automatically discovers the theme's functions and
  // templates that implement more targeted "suggestions" of generic theme
  // hooks. But suggestions implemented by a module must be explicitly
  // registered.
  $theme_hooks += array(
    'quotes_block__block' => array(
      'template' => 'quotes_block--block',
      'variables' => $theme_hooks['quotes_block']['variables'],
    ),
  );
  return $theme_hooks;
}

// ********************************************************************
// * Themeable Functions
// ********************************************************************

/**
 * Theamable function that displays the author as is or as a link.
 */
function theme_quotes_author(&$variables) {
  $node = $variables['node'];
  $format = $variables['format'];

  // Add Author if present.
  if (!empty($node->quotes_author)) {
    $author = $node->quotes_author;
    if (variable_get('quotes_author_link', FALSE)) {
      $author = l($author, "quotes/author/{$author}", array(
        'title' => t('Read more about %author', array(
          '%author' => $author,
        )),
      ));
      $author = decode_entities($author);
    }
    else {
      $author = check_markup($author, $format, $node->language, FALSE);
    }
    $node->content['quotes_author'] = array(
      '#prefix' => '<div class="quotes-author">' . variable_get('quotes_leader', '&mdash;') . ' ',
      '#suffix' => '</div>',
      '#markup' => $author,
    );
  }
  $variables['node'] = $node;
}

/**
 * Theamable function that displays the authors citation.
 */
function theme_quotes_citation(&$variables) {
  $node = $variables['node'];
  $format = $variables['format'];

  // Are we in a quotes_block?
  $in_block = isset($node->in_block) && $node->in_block;

  // Add citation if present.
  if (!empty($node->quotes_citation)) {

    // Check to see if we want the citation to show.
    if ($in_block && isset($node->show_citation) && $node->show_citation == 0) {

      // Do not display citation for this block.
      $node->quotes_citation = '';
    }
    else {
      $node->content['quotes_citation'] = array(
        '#prefix' => '<div class="quotes-citation"><cite>',
        '#suffix' => '</cite></div>',
        '#markup' => check_markup($node->quotes_citation, $format, $node->language, FALSE),
      );
    }
  }
  $variables['node'] = $node;
}

/**
 * Themeable function that displays a single quote and optional author.
 *
 * @ingroup themeable
 */
function theme_quotes_quote($variables) {
  if (!isset($variables['teaser'])) {
    $variables['teaser'] == FALSE;
  }
  if (!isset($variables['show_bio'])) {
    $variables['show_bio'] == variable_get('quotes_author_bio', FALSE);
  }
  if (!isset($variables['max_length'])) {
    $variables['max_length'] == 0;
  }
  $node = $variables['node'];
  $teaser = $variables['teaser'];
  $show_bio = $variables['show_bio'];
  $max_length = $variables['max_length'];
  global $user;
  $body = render(field_view_value('node', $node, 'body', 0, $teaser ? 'teaser' : 'full'));
  if ($max_length) {

    // We want to limit the text.
    $text = text_summary($body, $node->format, $size = $max_length);
    if (drupal_strlen($text) < drupal_strlen($body)) {
      $text .= '&hellip;' . l(t('(more)'), drupal_get_path_alias('node/' . $node->nid));
    }
  }
  else {
    $text = $body;
  }
  $leader = variable_get('quotes_leader', '&mdash;') . ' ';

  // Get the format for author and citation.
  $format = variable_get('quotes_format', filter_default_format());
  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, $format, $node->language, FALSE) : '';
  }
  switch ($show_bio) {
    case 1:
      $bio = $node->quotes_bio ? '<div class="quotes-bio">' . check_markup($node->quotes_bio, $format, $node->language, FALSE) . '</div>' : '';
      break;
    case 2:
      $menu_info = menu_get_item('quotes');
      $menu_title = drupal_strtolower($menu_info['title']);
      $bio = $node->quotes_author ? '<div class = "quotes-bio-link">' . l(t('See biography and !menu', array(
        '!menu' => $menu_title,
      )), '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>", $format, $node->language, FALSE) : '';
}

/**
 * Theme function to display quotes page possibly restricted to a certain user.
 *
 *
 * @ingroup themeable
 */
function theme_quotes_page($variables) {
}

/**
 * Themeable function that displays a block of quotes.
 *
 * @ingroup themeable
 */
function theme_quotes_block(&$variables) {
  $nodes = $variables['nodes'];
  $block = $variables['block'];
  $view_text = $block['view_text'];
  $more_text = $block['more_text'];
  $rand_freq = $block['rand_freq'];
  if (isset($variables['content'])) {
    $content = $variables['content'];
  }
  else {
    $content = array();
  }
  foreach ($nodes as $quote) {

    // This is necessary for shared objects because PHP doesn't copy objects,
    // but passes them by reference.  So when the objects are cached it can
    // result in the wrong output being displayed on subsequent calls.  The
    // cloning and unsetting of $node->content prevents the block output from
    // being the same as the node output.
    $quote = clone $quote;
    unset($quote->content);

    // Save first node title for possible block title below.
    if (!isset($blk_title)) {
      $blk_title = filter_xss($quote->title);
      $blk_nid = $quote->nid;
      $blk_uid = $quote->uid;
    }
    $block['delta'] = $block['bid'];
    $block['module'] = 'quotes';

    // Set $teaser to false so the length is properly processed.
    $quote->max_length = $block['max_length'];
    $quote->in_block = TRUE;
    $quote->show_titles = $block['show_titles'];
    $quote->more_text = $more_text;
    $quote->view_text = $view_text;
    $quote->show_citation = $block['show_citation'];

    // Get the format for author and citation.
    $format = variable_get('quotes_format', filter_default_format());
    $quote->content = array(
      '#prefix' => '<div class= "block-quotes clearfix">',
      '#suffix' => '</div>',
    );

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

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

        // Plain text.
        $quote->content['title'] = array(
          '#markup' => '<h3>' . t(check_plain($quote->title)) . '</h3>',
        );
        break;
    }

    // Is the comments installed and enambled, if so do we wish to display
    // a link to access them?
    if (module_exists('comment') && isset($quote->comment) == COMMENT_NODE_OPEN && !empty($view_text)) {
      $quote->content['view_link'] = array(
        '#prefix' => '<div class="quotes-view-link">',
        '#suffix' => '</div>',
        '#markup' => drupal_get_path_alias(l($view_text, 'node/' . $quote->nid)),
      );
    }
    $content[] = node_view($quote, 'full');
  }
  $_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', array(
      'account' => user_load($blk_uid),
    )),
  );
  $block['content'] = $content;
  $block['subject'] = strtr($block['name'], $_quotes_subs);
  $variables['block'] = $block;
  $variables['nodes'] = '';
}

// ********************************************************************
// * Module Functions
// ********************************************************************

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

    // 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();
      }
    }
  }
  $q_snippet = 'n.status = ' . (int) ($filters['block_type'] != 2);
  $query = db_select('quotes', 'q');
  $n_alias = $query
    ->join('node', 'n', 'q.vid=n.vid');
  $query
    ->fields('n', array(
    'nid',
  ));
  quotes_block_join_sql($filters, $query);
  $query
    ->where($q_snippet)
    ->condition('n.type', 'quotes');
  if (!$promoted_only) {
    $query
      ->condition('q.promote', 1);
    quotes_block_where_sql($filters, $query);
  }
  if ($filters['block_type'] == 0) {
    $query
      ->orderRandom();

    /* Type=0 is random. */
  }
  else {
    $query
      ->orderBy('n.created', 'DESC');
  }
  $query
    ->range(0, $limit);
  return $result = $query
    ->execute()
    ->fetchCol();
}

/**
 * Adds the necessary joins to a quotes block query to support filter criteria.
 *
 * @param array $filters
 *   The array specifying filter criteria using the keys rid_filter and
 *   tid_filter.
 * @param string $query
 *   The query object to modify.
 *
 * @return string
 *   The modified query containing any SQL joins necessary for the provided
 *   criteria.
 */
function quotes_block_join_sql($filters = array(), $query) {
  if (!empty($filters['rid_filter']) && $filters['rid_filter'] != 'none') {
    $ur_alias = $query
      ->leftjoin('users_roles', 'a', 'uid');
  }
  if (!empty($filters['tid_filter']) && $filters['tid_filter'] != 'none') {
    $ttn_alias = $query
      ->join('taxonomy_index', 'ti', 'n.nid = ti.nid');
  }
  return $query;
}

/**
 * Modifies the SQL query as needed with conditions provided by filter criteria.
 *
 * @param array $filters
 *   The array specifying filter criteria using the keys nid_filter,
 *   rid_filter, uid_filter, and tid_filter.
 * @param string $query
 *   The sql query we are modifying.
 *
 * @return string
 *   The modified query containing any conditions necessary for the provided
 *   criteria.
 */
function quotes_block_where_sql($filters = array(), $query) {

  // Filter by specific node IDs.
  if ($filters['nid_filter']) {
    $nid_nums = explode(",", $filters['nid_filter']);
    $query
      ->condition('n.nid', $nid_nums, 'IN');
  }

  // Filter by specific author IDs.
  if ($filters['aid_filter'] && $filters['aid_filter'] != 'none') {
    $aid_nums = explode(",", $filters['aid_filter']);
    $query
      ->condition('q.aid', $aid_nums, 'IN');
  }
  if ($filters['rid_filter'] && $filters['rid_filter'] != 'none') {

    // $f = $filters['rid_filter'];
    // $ar = $aliases['users_roles'];
    // $an = $aliases['node'];
    // $where[] = sprintf(" ($ar.rid IN ($f) OR (%d IN ($f) AND $an.uid = 0)) ", DRUPAL_ANONYMOUS_RID);
    $ur_nums = explode(",", $filters['rid_filter']);
    $query
      ->condition(db_or()
      ->condition('ur.rid', $ur_nums, 'IN')
      ->condition(db_and()
      ->condition('n.uid', 0)
      ->condition('qb.rid_filter', DRUPAL_ANONYMOUS_RID, 'IN')));
  }
  if ($filters['uid_filter'] && $filters['uid_filter'] != 'none') {
    $id_nums = explode(",", $filters['uid_filter']);
    $query
      ->condition('n.uid', $id_nums, 'IN');
  }
  if ($filters['tid_filter'] && $filters['tid_filter'] != 'none') {
    $tid_nums = explode(",", $filters['tid_filter']);
    $query
      ->condition('ti.tid', $tid_nums, 'IN');
  }
}

/**
 * Menu callback that generates a list of quotes created by the current user.
 */
function quotes_myquotes() {
  global $user;
  return quotes_page($user->uid, $arg2 = NULL);
}

/**
 * Menu callback that displays a page of quotes restricted to a certain user.
 *
 * @param array $arg1
 *   The user object (from menu) of the user's quotes to be displayed.
 * @param string $arg2
 *   - 'feed', a feed is requested.
 *   - '- FIND -', author selection requested.
 *
 * @return array
 *   A renderable array to display a page of quotes.
 */
function quotes_page($arg1 = NULL, $arg2 = NULL) {
  if (is_null($arg1)) {
    global $user;
  }
  else {
    $user = user_load($arg1);
  }

  // Breadcrumb navigation. The default is wrong because of a core menu bug.
  $breadcrumb = array();
  $breadcrumb[] = l(t('Home'), variable_get('site_frontpage', 'node'));
  $menu_info = menu_get_item('quotes');
  $menu_title = $menu_info['title'];
  $breadcrumb[] = l($menu_title, 'quotes');
  drupal_set_breadcrumb($breadcrumb);
  if (isset($arg2)) {

    // Requesting an author.
    return quotes_author($arg2);
  }
  $query = db_select('node', 'n')
    ->extend('PagerDefault');
  $nr_alias = $query
    ->join('node_revision', 'nr', 'nr.vid = n.vid');
  $query
    ->fields('n', array(
    'nid',
  ))
    ->condition('n.status', '1')
    ->condition('n.type', 'quotes');
  $menu_title = '';
  $url = url('quotes/feed', array(
    'absolute' => TRUE,
  ));
  if (isset($arg1)) {
    $account = user_load($arg1);
    if ($account) {
      if ($account->uid) {
        $name = isset($account->realname) ? $account->realname : $account->name;
        $menu_info = menu_get_item('quotes');
        $menu_title = $menu_info['title'];
        drupal_set_title(t("!name's !menu", array(
          '!menu' => $menu_title,
          '!name' => $name,
        )), PASS_THROUGH);
        $url = url('quotes/' . $account->uid . '/feed', array(
          'absolute' => TRUE,
        ));
        $query
          ->condition('n.uid', $account->uid);
      }
      else {
        $name = DRUPAL_ANONYMOUS_RID;
      }
    }
  }
  $name = isset($name) ? $name : '';
  $query
    ->orderBy('n.sticky', 'DESC')
    ->orderBy('n.created', 'DESC')
    ->addTag('node_access');
  $nids = $query
    ->limit(variable_get('quotes_per_page', 10))
    ->execute()
    ->fetchCol();
  if ($nids) {
    if (isset($arg1) && !$account) {
      drupal_set_message(t('Sorry, the account ID supplied is invalid.'));
    }
  }
  else {
    if (!$arg1) {
      drupal_set_message(t('You have not created any quotes.'));
    }
    else {
      drupal_set_message(t('!author has not created any quotes or you do not have permission to view them.', array(
        '!author' => theme('username', array(
          'account' => $account,
        )),
      )));
    }
  }
  $nodes = node_load_multiple($nids);

  // Reset node value if required.
  foreach ($nodes as $node) {

    // Undo sticky flag.
    if ($node->sticky) {
      $node->sticky = 0;
    }

    // Since this is a Page View, use the nid for empty titles.
    if (empty($node->title)) {
      $node->title = $node->nid;
    }
  }
  $build['quotes_list'][] = node_view_multiple($nodes, $view_mode = 'full', $language = NULL);
  if ($name) {
    drupal_add_feed($url, t("RSS - Favorite @title of !name", array(
      '!name' => $name,
      '@title' => $menu_title,
    )));
  }
  else {
    drupal_add_feed($url, t("RSS - Famous and not so famous quotes"));
  }
  $build['pager'] = array(
    '#theme' => 'pager',
    '#weight' => 5,
  );
  return $build;
}

/**
 * Menu callback that selects quotes based on author.
 *
 * @param string $author
 *   The name of the author of the quotes.
 *
 * @return array
 *   A renderable array to display a page of quotes by author.
 */
function quotes_author($author = NULL) {

  // Get a count of our authors.
  $count = db_query("SELECT COUNT(*) FROM {quotes_authors}")
    ->fetchField();

  // Only one author present, display their quotes.
  if ($count == 1) {
    $author = $count;
  }

  // Check to see if we want to find an anthor.
  if ($author === "- FIND -") {
    return drupal_get_form('quotes_author_form');
  }
  if ($author === 0 || $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_query("SELECT name, bio FROM {quotes_authors} WHERE aid = :aid", array(
        ':aid' => $aid,
      ))
        ->fetchAssoc();
      $author = $auth['name'];
    }
    else {
      $auth = db_query("SELECT aid, bio FROM {quotes_authors} WHERE name = :author", array(
        ':author' => $author,
      ))
        ->fetchAssoc();
      $aid = $auth['aid'];
    }
    if (!$aid) {

      // Unspecified author.
      drupal_set_message(t("I couldn't locate that author."));
      return drupal_get_form('quotes_author_form');
    }
  }

  // Get the format for author and citation.
  $format = variable_get('quotes_format', filter_default_format());
  $build = array();
  variable_set('quotes_bio_set', FALSE);
  $limit = variable_get('quotes_per_page', 10);
  $menu_info = menu_get_item('quotes');
  $menu_title = $menu_info['title'];
  if (!$author) {
    $author = 'Unknown Authors';
  }
  drupal_set_title(decode_entities(t('!menu by !name', array(
    '!menu' => $menu_title,
    '!name' => $author,
  ))), PASS_THROUGH);
  $sql = db_select('node', 'n')
    ->extend('PagerDefault');
  $nr_alias = $sql
    ->join('node_revision', 'nr', 'nr.vid = n.vid');
  $q_alias = $sql
    ->join('quotes', 'q', 'q.vid = nr.vid');
  $sql
    ->fields('n', array(
    'nid',
  ))
    ->condition('q.aid', $aid)
    ->condition('nr.status', '1')
    ->condition('n.type', 'quotes')
    ->orderBy('nr.sticky', 'DESC')
    ->orderBy('n.created', 'DESC')
    ->addTag('node_access');
  $result = $sql
    ->limit($limit)
    ->execute()
    ->fetchCol();
  if (!empty($result)) {
    $nodes = node_load_multiple($result);

    // Undo sticky flag.
    foreach ($nodes as $node) {
      if ($node->sticky) {
        $node->sticky = 0;
      }
    }
    $build['quotes_list'][] = node_view_multiple($nodes, $view_mode = 'full', $language = NULL);
    $build['pager'] = array(
      '#theme' => 'pager',
      '#weight' => 5,
    );
  }
  else {
    if (isset($account)) {
      if (!$account) {
        drupal_set_message(t('No quotes have been created.'));
      }
      elseif (isset($account) && $account->uid == $user->uid) {
        drupal_set_message(t('You have not created any quotes.'));
      }
    }
  }
  return $build;
}

/**
 * Form to select an author.
 */
function quotes_author_form($form, $form_state) {
  $form = array();
  $max_name_length = 78;
  $authors = quotes_get_authors();
  if (!is_array($authors) || empty($authors)) {
    form_set_error('body', t('No Authors Found'));
    return $form;
  }
  $author_names = array();
  foreach ($authors as $aid => $name) {

    // Limit the length.
    if (drupal_strlen($name) > $max_name_length) {
      $name = drupal_substr($name, 0, $max_name_length - 3) . '...';
    }

    // TODO - check why when we check_plain $name ',",etc. get stripped
    // use filter_xss for now.
    $author_names[$aid] = filter_xss($name);
  }
  $form['author'] = array(
    '#type' => 'select',
    '#options' => $author_names,
    '#size' => min(20, count($authors)),
    '#description' => t('Choose 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, &$form_state) {
  $form_state['redirect'] = 'quotes/author/' . $form_state['values']['author'];
}

/**
 * Displays an RSS feed containing recent quotes of a given user.
 *
 * @param array $account
 *   The account of the user's quotes to be fed.
 */
function _quotes_feed_user($account = NULL) {
  $luser = user_load(arg(1));
  $sql = db_select('node', 'n')
    ->fields('n', array(
    'nid',
    'title',
    'created',
    'uid',
  ))
    ->condition('status', 1)
    ->condition('type', 'quotes')
    ->condition('uid', $luser->uid)
    ->orderBy('created', 'DESC')
    ->range(0, variable_get('feed_default_items', 15))
    ->addTag('node_access')
    ->execute()
    ->fetchCol();
  $channel['title'] = t("!name's quotes", array(
    '!name' => $luser->uid ? $luser->name : variable_get('anonymous', t('Anonymous')),
  ));
  $channel['link'] = url("quotes/" . $luser->uid, array(
    'absolute' => TRUE,
  ));
  node_feed($sql, $channel);
}

/**
 * Displays an RSS feed containing recent quotes of all users.
 */
function _quotes_feed_last() {
  $sql = db_select('node', 'n')
    ->fields('n', array(
    'nid',
    'title',
    'created',
    'uid',
  ))
    ->condition('status', 1)
    ->condition('type', 'quotes')
    ->orderBy('created', 'DESC')
    ->range(0, variable_get('feed_default_items', 15))
    ->addTag('node_access')
    ->execute()
    ->fetchCol();
  $channel['title'] = t('!site_name quotes', array(
    '!site_name' => variable_get('site_name', 'Authentic Empowerment'),
  ));
  $channel['link'] = url('quotes/' . arg(1), array(
    'absolute' => TRUE,
  ));
  node_feed($sql, $channel);
}

/**
 * Parses and returns the quotes contained in the provided node body.
 *
 * @param array $node
 *   The node object containing the data to be parsed.
 * @param bool $set_errors
 *   The boolean indicating whether or not form errors should be set.
 *
 * @return array
 *   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();

  // This lets us dump a % if it's on the end of the author.
  $trim_chars = " \t\n\r\0\v%";
  if ($node->quotes_import_format == 'text') {

    // The documentation shows '<tab>' and some users have actually used
    // that string, so let's allow it.
    $quote_import = str_replace("<tab>", "\t", $node->body[$node->language][0]['value']);
    foreach (explode("\r", str_replace("\\\r", "\n", preg_replace('<(?:\\r\\n?|\\n)>', "\r", trim($quote_import)))) 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. "@found"', array(
            '!num' => count($quotes) + 1,
            '@found' => $quote[0],
          )));
        }
        break;
      }
      if (!isset($quote[1])) {
        $quote[1] = NULL;
      }
      if (!isset($quote[2])) {
        $quote[2] = NULL;
      }

      // Check for length of author.
      if (drupal_strlen($quote[1]) > 254) {
        form_set_error('body', t('Parse error on quote !num. "@found"', array(
          '!num' => count($quotes) + 1,
          '@found' => $quote[0],
        )));
      }
      else {
        $quotes[] = (object) array(
          'body' => trim($quote[0]),
          'quotes_author' => trim(check_plain($quote[1])),
          'quotes_citation' => trim(check_markup($quote[2])),
          'format' => $node->body[$node->language][0]['format'],
        );
      }
    }
  }
  elseif ($node->quotes_import_format == 'fortune') {
    foreach (preg_split('<\\n+%+\\n+>', str_replace("\t", '    ', preg_replace('<(?:\\r\\n?|\\n)>', "\n", $node->body[$node->language][0]['value']))) as $quote) {
      if (preg_match('<^(?:(?:(.*)\\n+\\s*--\\s*(.*?)))$>s', $quote, $matches)) {
        $aquote = explode("--", $matches[0]);
        if (!isset($aquote[1])) {
          $aquote[1] = NULL;
        }
        if (!isset($aquote[2])) {
          $aquote[2] = NULL;
        }
        if (drupal_strlen($aquote[1]) > 254) {
          form_set_error('body', t('Parse error on quote !num. "@found"', array(
            '!num' => count($quotes) + 1,
            '@found' => $aquote[0],
          )));
        }
        else {
          $quotes[] = (object) array(
            'body' => trim($aquote[0]),
            'quotes_author' => trim(check_plain($aquote[1])),
            'quotes_citation' => trim(check_markup($aquote[2])),
            'format' => $node->body[$node->language][0]['format'],
          );
        }
      }
    }
  }
  elseif ($set_errors) {
    form_set_error('quotes_import_format', t('Please select a valid import format. (!format)', array(
      '!format' => $node->quotes_import_format,
    )));
  }
  return $quotes;
}

/**
 * Produce an array of all authors.
 *
 * @param array $none
 *   Returns a list of authors.
 *
 * @return array
 *   An associative array of authors in the quotes table.
 */
function quotes_get_authors($none = FALSE) {
  if ($none) {
    $list = array(
      0 => '- ' . t('none') . ' -',
    );
  }
  else {
    $list = array();
  }
  $unknown = -1;
  $sql = db_select('quotes_authors', 'qa');
  $sql
    ->fields('qa', array(
    'aid',
    'name',
  ));
  $q_alias = $sql
    ->leftjoin('quotes', 'q', "q.aid = qa.aid");
  $count_alias = $sql
    ->addExpression('COUNT(q.nid)', 'count');
  $sql
    ->groupBy('name, qa.aid')
    ->orderBy('name', 'ASC');
  $result = $sql
    ->execute()
    ->fetchAll();
  foreach ($result as $key => $value) {
    $row = $value;
    if (empty($row->name)) {
      $unknown = $row->aid;
    }
    if ($row->count) {
      $list[$row->aid] = $row->name;
    }
    else {
      db_query("DELETE FROM {quotes_authors} WHERE aid=:aid", array(
        ':aid' => $row->aid,
      ));
      watchdog('Quotes', '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(
    ' ' => t('unspecified'),
  );
  $sql = db_select('quotes', 'q');
  $q_cite = $sql
    ->addField('q', 'citation');
  $sql
    ->orderBy('citation')
    ->distinct();
  $result = $sql
    ->execute()
    ->fetchAll();
  foreach ($result as $key => $row) {
    $list[$row->citation] = $row->citation;
  }
  return $list;
}

/**
 * Function to provide autocomplete for author field.
 *
 * @param string $string
 *   The string currently entered by the user.
 *
 * @return array
 *   An array of matches in JSON format.
 */
function _quotes_autocomplete_author($string) {
  $matches = array();
  $strlen = drupal_strlen($string);
  if ($strlen) {
    $result = db_query("SELECT name FROM {quotes_authors} WHERE LOWER(name) LIKE LOWER(:string)", array(
      ':string' => ($strlen > 1 ? '%%' : '') . addslashes($string) . '%%',
    ));
    foreach ($result as $row) {
      $matches[$row->name] = check_plain($row->name);
    }
  }
  drupal_json_output($matches);
}

/**
 * Function to provide autocomplete for citation field.
 *
 * @param string $string
 *   The string currently entered by the user.
 *
 * @return array
 *   An array of matches in JSON format.
 */
function _quotes_autocomplete_citation($string) {
  $matches = array();
  $strlen = drupal_strlen($string);
  if ($strlen) {
    $result = db_query("SELECT citation FROM {quotes} WHERE LOWER(citation) LIKE LOWER(:string)", array(
      ':string' => ($strlen > 1 ? '%%' : '') . addslashes($string) . '%%',
    ));
    foreach ($result as $row) {
      $matches[$row->citation] = check_plain($row->citation);
    }
  }
  drupal_json_output($matches);
}

/**
 * Implements hook_validate().
 */
function quotes_validate($node, &$form) {

  // Bail if we are doing a single quote.
  if ($node->quotes_import_format == 'single') {
    return;
  }
  _quotes_parse_import($node, TRUE);
}

/**
 * Implements 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;
}

/**
 * Implements 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;
}

/**
 * Implements hook_views_api().
 */
function quotes_views_api() {
  return array(
    'api' => 2,
    'path' => drupal_get_path('module', 'quotes'),
  );
}

/**
 * Implements hook_views_handlers().
 */
function quotes_views_handlers() {
  return array(
    'info' => array(
      'path' => drupal_get_path('module', 'quotes'),
    ),
    'handlers' => array(
      'views_handler_field_quotes' => array(
        'parent' => 'views_handler_field',
      ),
    ),
  );
}

/**
 * Implements hook_field_extra_fields().
 */
function quotes_field_extra_fields() {
  $extra['node']['quotes'] = array(
    'display' => array(
      'quotes_author' => array(
        'label' => t('Quotes author'),
        'description' => t('Quote Authors'),
        'weight' => 5,
      ),
      'quotes_citation' => array(
        'label' => t('Quotes citation'),
        'description' => t('Quote citationss'),
        'weight' => 10,
      ),
      'quotes_bio' => array(
        'label' => t('Quotes bio'),
        'description' => t('Author biography'),
        'weight' => -90,
      ),
    ),
  );
  return $extra;
}

Functions

Namesort descending Description
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_block_configure Implements hook_block_configure().
quotes_block_configure_save Quotes block configuration save.
quotes_block_info Implements hook_block_info().
quotes_block_join_sql Adds the necessary joins to a quotes block query to support filter criteria.
quotes_block_mb_blocked Quotes multi-block blocking.
quotes_block_save Implements hook_block_save().
quotes_block_view Implements hook_block_view().
quotes_block_where_sql Modifies the SQL query as needed with conditions provided by filter criteria.
quotes_cron Implements hook_cron().
quotes_delete Implements hook_delete().
quotes_field_extra_fields Implements hook_field_extra_fields().
quotes_form Implements hook_form().
quotes_form_alter Implements 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 random quote or the most recent quote ID based on filter criteria.
quotes_help Implements hook_help().
quotes_import Inserts quotes data into db tables and handles %id variable in quotes title.
quotes_init Implements hook_init().
quotes_insert Implements hook_insert().
quotes_load Implements hook_load().
quotes_menu Implements hook_menu().
quotes_menu_alter Implements hook_menu_alter().
quotes_myquotes Menu callback that generates a list of quotes created by the current user.
quotes_node_access Implements hook_node_access().
quotes_node_info Implements hook_node_info().
quotes_node_presave Form submit presave handler for importing quotes.
quotes_node_view Implements hook_node_view().
quotes_page Menu callback that displays a page of quotes restricted to a certain user.
quotes_permission Implements hook_permission().
quotes_preprocess_node Implements hook_preprocess_node().
quotes_theme Implements hook_theme().
quotes_token_list Implements hook_token_list().
quotes_token_values Implements hook_token_values().
quotes_update Implements hook_update().
quotes_validate Implements hook_validate().
quotes_view Implements hook_view().
quotes_views_api Implements hook_views_api().
quotes_views_handlers Implements hook_views_handlers().
theme_quotes_author Theamable function that displays the author as is or as a link.
theme_quotes_block Themeable function that displays a block of quotes.
theme_quotes_citation Theamable function that displays the authors citation.
theme_quotes_page Theme function to display quotes page possibly restricted to a certain user.
theme_quotes_quote Themeable function that displays a single quote and optional author.
_quotes_add Add a quote.
_quotes_autocomplete_author Function to provide autocomplete for author field.
_quotes_autocomplete_citation Function to provide autocomplete for citation field.
_quotes_block_configuration_validate Validates that changes made on the block configuration screen are valid.
_quotes_block_configure Quotes block configuration.
_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_myquotes_access Menu access callback.
_quotes_parse_import Parses and returns the quotes contained in the provided node body.
_quotes_recentquotes_access Menu access callback.
_quotes_settings Menu call to the quotes settings page.

Constants