You are here

classified.module in Classified Ads 7.3

Same filename and directory in other branches
  1. 6.3 classified.module

A pure D7 classified ads module inspired by the ed_classified module.

@copyright (c) 2010-2011 Ouest Systèmes Informatiques (OSInet)

@license General Public License version 2 or later

Original code implementing a feature set derived from ed_classified.

---- Information about ed_classified ---- Michael Curry, Exodus Development, Inc. exodusdev@gmail.com for more information, please visit: http://exodusdev.com/drupal/modules/ed_classified.module

Copyright (c) 2006, 2007 Exodus Development, Inc. All Rights Reserved.

Licensed under the terms of the GNU Public License (GPL) version 2. Please see LICENSE.txt for license terms. Possession and use of this code signifies acceptance of license terms. ---- /ed-classified ----

File

classified.module
View source
<?php

/**
 * @file
 * A pure D7 classified ads module inspired by the ed_classified module.
 *
 * @copyright (c) 2010-2011 Ouest Systèmes Informatiques (OSInet)
 *
 * @license General Public License version 2 or later
 *
 * Original code implementing a feature set derived from ed_classified.
 *
 * ---- Information about ed_classified ----
 * Michael Curry, Exodus Development, Inc.
 * exodusdev@gmail.com
 * for more information, please visit:
 *   http://exodusdev.com/drupal/modules/ed_classified.module
 *
 * Copyright (c) 2006, 2007 Exodus Development, Inc.  All Rights Reserved.
 *
 * Licensed under the terms of the GNU Public License (GPL) version 2.  Please
 * see LICENSE.txt for license terms.  Possession and use of this code signifies
 * acceptance of license terms.
 * ---- /ed-classified ----
 */

/**
 * Implements hook_block() for ('view', 'popular').
 */
function _classified_block_view_popular() {
  if (!module_exists('statistics')) {
    $ret = NULL;
  }
  else {
    $limit = _classified_get('popular-count');
    $vid = _classified_get('vid');

    /** @var SelectQuery $q */
    $q = db_select('node', 'n')
      ->comment(__FUNCTION__);
    $nc = $q
      ->leftJoin('node_counter', 'nc', 'n.nid = nc.nid');
    $q
      ->innerJoin('taxonomy_index', 'ti', 'n.nid = ti.nid');
    $td = $q
      ->innerJoin('taxonomy_term_data', 'td', 'ti.tid = td.tid');
    $results = $q
      ->fields('n', array(
      'nid',
      'title',
    ))
      ->fields($td, array(
      'name',
    ))
      ->condition('n.status', 1)
      ->condition('n.type', 'classified')
      ->condition("{$td}.vid", $vid)
      ->addTag('node_access')
      ->orderBy("{$nc}.totalcount", 'DESC')
      ->orderBy("{$nc}.daycount", 'DESC')
      ->orderBy('n.title', 'ASC')
      ->orderBy('n.nid', 'DESC')
      ->range(0, $limit)
      ->execute();
    $ads = array();
    foreach ($results as $result) {
      $title = t('!title (!category)', array(
        '!title' => $result->title,
        '!category' => $result->name,
      ));
      $ads[] = l($title, 'node/' . $result->nid);
    }
    $ret = array(
      'subject' => t('Popular ads'),
      'content' => count($ads) ? array(
        '#theme' => 'item_list__classified_popular',
        '#items' => $ads,
      ) : t('No ad viewed yet.'),
    );
  }
  return $ret;
}

/**
 * Implements hook_block() for ('view', 'recent').
 */
function _classified_block_view_recent() {
  $limit = _classified_get('recent-count');
  $vid = _classified_get('vid');

  /** @var SelectQuery $q */
  $q = db_select('node', 'n')
    ->comment(__FUNCTION__);
  $q
    ->innerJoin('taxonomy_index', 'ti', 'n.nid = ti.nid');
  $td = $q
    ->innerJoin('taxonomy_term_data', 'td', 'ti.tid = td.tid');
  $results = $q
    ->fields('n', array(
    'nid',
    'title',
  ))
    ->fields($td, array(
    'name',
  ))
    ->condition('n.status', 1)
    ->condition('n.type', 'classified')
    ->condition("{$td}.vid", $vid)
    ->orderBy('n.created', 'DESC')
    ->orderBy('n.changed', 'DESC')
    ->orderBy('n.title', 'ASC')
    ->addTag('node_access')
    ->range(0, $limit)
    ->execute();
  $ads = array();
  foreach ($results as $result) {
    $title = t('!title (!category)', array(
      '!title' => $result->title,
      '!category' => $result->name,
    ));
    $ads[] = l($title, 'node/' . $result->nid);
  }
  $ret = array(
    'subject' => t('Recent ads'),
    'content' => count($ads) ? array(
      '#theme' => 'item_list__classified_recent',
      '#items' => $ads,
    ) : array(
      '#markup' => t('No ad viewed yet.'),
    ),
  );
  return $ret;
}

/**
 * Implements hook_block() for ('view', 'stats').
 *
 * No node_access control on stats, hence no addTag('node_access').
 */
function _classified_block_view_stats() {
  $yesterday = REQUEST_TIME - 24 * 60 * 60;

  /** @var SelectQuery $q */
  $q = db_select('node', 'n')
    ->comment(__FUNCTION__);
  $q
    ->addExpression('COUNT(n.nid)', 'nid_count');
  $changed = $q
    ->addExpression('n.changed > :yesterday', 'changed', array(
    ':yesterday' => $yesterday,
  ));
  $status = $q
    ->addField('n', 'status');
  $results = $q
    ->condition('n.type', 'classified')
    ->groupBy($changed)
    ->groupBy($status)
    ->execute();
  $stats = array(
    0 => array(
      0 => 0,
      1 => 0,
    ),
    1 => array(
      0 => 0,
      1 => 0,
    ),
  );
  foreach ($results as $result) {
    $stats[$result->status][$result->changed] = $result->nid_count;
  }
  $rows = array();
  $rows[] = array(
    t('1 day ad updates'),
    $stats[1][1],
  );
  $rows[] = array(
    t('Active ads'),
    $stats[1][1] + $stats[1][0],
  );
  $rows[] = array(
    t('Expired ads'),
    $stats[0][0] + $stats[0][1],
  );
  $ret = array(
    'subject' => t('Ad activity'),
    'content' => array(
      '#theme' => 'table__classified_stats',
      '#header' => NULL,
      '#rows' => $rows,
      '#attributes' => array(),
    ),
  );
  return $ret;
}

/**
 * A simplified alternative to variable_get().
 *
 * Prepends the name of the module and a dash, and automatically obtains the
 * default value if needed, as expected for D8.
 *
 * @param string $name
 *   The name of the variable to return.
 *
 * @return mixed
 *   The value of the variable.
 */
function _classified_get($name) {
  $vars = _classified_get_vars();
  $name = 'classified-' . $name;
  return variable_get($name, $vars[$name]);
}

/**
 * Build a breadcrumb trail for an a Classified Ad node.
 *
 * @param object $node
 *   A fully loaded Classified Ad node.
 *
 * @return array
 *   A breadcrumb trail array.
 */
function _classified_get_breadcrumb_by_node($node) {
  $terms = _classified_taxonomy_node_get_terms_by_vocabulary($node, _classified_get('vid'));
  $term = empty($terms) ? NULL : reset($terms);
  $ret = _classified_get_breadcrumb_by_term($term, TRUE);
  return $ret;
}

/**
 * Build a Classified Ad breadcrumb trail for an a term.
 *
 * @param object $term
 *   A fully loaded term for which a trail must be built.
 * @param bool $include_last
 *   When building a breadcrumb trail for a term, it should not be included, as
 *   it will be the last component of the BC. However, when building for another
 *   type of page, like a node page, it needs to be included, as it is not the
 *   last component of the BC.
 *
 * @return array
 *   A breadcrumb trail array.
 */
function _classified_get_breadcrumb_by_term($term = NULL, $include_last = FALSE) {
  $bc = array();
  if (is_object($term) && $term->vid != _classified_get('vid')) {
    watchdog('classified', 'Building a breadcrumb trail for a term outside the Classified Ad vocabulary', array(), WATCHDOG_WARNING, l($term->name, 'admin/structure/taxonomy/edit/term/' . $term->tid));
  }

  // Worst case: array() for tid == 0.
  $parents = taxonomy_get_parents_all(isset($term->tid) ? $term->tid : 0);
  if (!$include_last && !empty($parents)) {

    // Remove current term.
    array_shift($parents);
  }
  foreach ($parents as $term) {
    array_unshift($bc, l($term->name, classified_term_path($term)));
  }
  array_unshift($bc, l(t('Classified Ads'), 'classified'));
  array_unshift($bc, l(t('Home'), '<front>'));
  return $bc;
}

/**
 * Convert a UNIX timestamp to the format used by the date widget.
 *
 * Note that this is a one-way conversion: hour/minute/second information is
 * lost in the conversion.
 *
 * @param int $ts
 *   A UNIX timestamp.
 *
 * @return array
 *   Or NULL for The Epoch ($ts == 0).
 *
 * @see _classified_timestamp_from_date()
 */
function _classified_get_date_from_timestamp($ts) {
  $date = getdate($ts);
  $ret = $ts ? array(
    'day' => $date['mday'],
    'month' => $date['mon'],
    'year' => $date['year'],
  ) : NULL;
  return $ret;
}

/**
 * Domain definition for notification kinds.
 *
 * @see classified_notify_kind_load()
 *
 * @return array
 *   The array of known notification kinds.
 */
function _classified_get_notify_kinds() {
  $ret = array(
    'pre-purge',
    'pre-expire',
    'half-life',
  );
  return $ret;
}

/**
 * Convert a UNIX timestamp from the format used by the date widget.
 *
 * @param mixed $date
 *   A NULL value will return The Epoch. Otherwise, pass an array.
 *
 * @return int
 *   The UNIX timestamp for 00:00:00 on that date.
 *
 * @see _classified_get_date_from_timestamp()
 */
function _classified_get_timestamp_from_date($date) {
  $ret = is_array($date) ? mktime(0, 0, 0, $date['month'], $date['day'], $date['year']) : 0;
  return $ret;
}

/**
 * List the module variables.
 *
 * This is a first step towards D8 hook_variable_info().
 * See @link http://drupal.org/node/145164 @endlink.
 *
 * No static cache: there is no reason for this function to be invoked multiple
 * times on a page.
 *
 * @return array
 *   A hash of the variables and their defaults
 */
function _classified_get_vars() {
  $vars = array(
    'vid' => 0,
    'max-length' => 500,
    'list-body' => 'empty',
    'date-format' => t('%d/%m/%Y'),
    // 2 weeks lifetime.
    'lifetimes' => array(
      0 => 2 * 7,
    ),
    // 1 week grace after expiration.
    'grace' => 7,
    'recent-count' => 5,
    'popular-count' => 5,
    // Editing a node sends it back to moderation (modr8) if available.
    'edit-modr8' => TRUE,
    // New in D7: field names.
    'field-category' => 'classified_category',
    // Optional classified_notifications module.
    'notifications-half-life-subject' => t('Classified Ad on [site:name]: half-life warning.'),
    'notifications-half-life-body' => t("Some of your Classified Ads on [site:name] have reached half their lifetime. They are listed below for your convenience.\n\nPlease visit your ads list at [user:classified-ads-url] if you wish to modify them.\n\n[user:classified-ads-plain]\n"),
    'notifications-pre-expire-subject' => t('Classified Ad on [site:name]: pre-expiration warning.'),
    'notifications-pre-expire-body' => t("Some of your Classified Ads on [site:name] will expire tomorrow. They are listed below for your convenience.\n\nPlease visit your ads list at [user:classified-ads-url] if you wish to extend their lifetime.\n\n[user:classified-ads-plain]\n"),
    'notifications-expire-subject' => t('Classified Ad expiration on [site:name] warning.'),
    'notifications-expire-body' => t("Some of your Classified Ads on [site:name] have expired. They are listed below for your convenience.\n\nPlease visit your ads list at [user:classified-ads-url] if you wish to renew them.\n\n[user:classified-ads-plain]\n"),
    'notifications-pre-purge-subject' => t('Classified Ad on [site:name]: pre-purge warning.'),
    'notifications-pre-purge-body' => t("Some of your expired Classified Ads on [site:name] will be deleted tomorrow. They are listed below for your convenience.\n\nPlease visit your ads list at [user:classified-ads-url] if you wish to avoid their deletion.\n\n[user:classified-ads-plain]\n"),
    'notifications-purge-subject' => t('Classified Ad purge on [site:name] warning.'),
    'notifications-purge-body' => t("Some of your expired Classified Ads on [site:name] have been purged. They are listed below for your convenience.\n\nPlease visit your ads list at [user:classified-ads-url] to check your remaining ads.\n\n[user:classified-ads-plain]\n"),
  );
  $ret = array();
  foreach ($vars as $name => $default) {
    $ret['classified-' . $name] = $default;
  }
  return $ret;
}

/**
 * Prepare a node listing from a DB query resource.
 *
 * @param resource $results
 *   A PDO results set returning node ids within objects.
 *
 * @return array
 *   A render array for the listing.
 */
function _classified_list_nodes($results) {
  $rows = array();
  $now = REQUEST_TIME;
  $list_body = _classified_get('list-body');
  $node_ids = array();
  foreach ($results as $result) {
    $node_ids[] = $result->id;
  }
  $date_format = _classified_get('date-format');
  switch ($list_body) {
    case 'empty':
    case 'body':
      $header = array(
        t('Contents'),
        t('Expires'),
      );
      break;
    case 'node':
    default:
      $header = NULL;
  }
  $nodes = node_load_multiple($node_ids);
  foreach ($nodes as $node) {
    switch ($list_body) {
      case 'empty':
        $cell = l($node->title, 'node/' . $node->nid);
        $row = array(
          $cell,
          strftime($date_format, $node->expires),
        );
        break;
      case 'body':
        $body_items = field_get_items('node', $node, 'body');
        $cell = array(
          'data' => array(
            'title' => array(
              '#markup' => l($node->title, 'node/' . $node->nid, array(
                'attributes' => array(
                  'class' => 'classified-list-title',
                ),
              )),
            ),
          ),
        );
        if (!empty($body_items)) {

          // Assume a single body item.
          $body_field = reset($body_items);
          $cell['data']['body'] = array(
            '#prefix' => '<span class="classified-list-teaser">',
            '#markup' => filter_xss(text_summary($body_field['value'], $body_field['format'])),
            '#suffix' => '</span>',
          );
        }
        $row = array(
          $cell,
          strftime($date_format, $node->expires),
        );
        break;
      case 'node':
        $cell = array(
          'data' => node_view($node, 'classified'),
        );
        $row = array(
          $cell,
        );
        break;

      // Should not happen.
      default:
        $row = array();
    }
    if ($list_body != 'node' && $node->expires < $now) {
      foreach ($row as $index => $cell) {
        $row[$index] = array(
          'data' => $cell,
          'class' => 'node-unpublished',
        );
      }
    }
    $rows[] = $row;
  }
  $ret = array(
    '#theme' => 'table',
    '#header' => $header,
    '#rows' => $rows,
  );
  return $ret;
}

/**
 * Page callback for 'classified'.
 *
 * Build a page listing ad categories and the number of ads in them. This is a
 * bit heavy on CPU so we cache the whole overview.
 *
 * @return array
 *   Render array for the listing.
 */
function _classified_page_overview() {
  $cid = 'classified:overview';
  $admin_access = user_access('administer nodes') || user_access('administer classified ads');
  $cached = $admin_access ? FALSE : cache_get($cid);
  if ($cached) {
    list($header, $rows, $total) = empty($cached->data) ? array(
      array(),
      array(),
      0,
    ) : $cached->data;

    // Just in case you'd want to give a hint that the data is not really fresh.
    $ret['table']['#attributes']['class']['classified-cached'] = 'classified-cached';
  }
  else {
    $vid = _classified_get('vid');
    $terms = taxonomy_get_tree($vid);
    $header = array(
      t('Ad category'),
      array(
        'class' => 'classified-number',
        'data' => t('# Ads'),
      ),
    );
    $rows = array();
    $total = 0;
    foreach ($terms as $term) {
      $q = new EntityFieldQuery();
      $q
        ->fieldCondition('classified_category', 'tid', $term->tid, '=', 0)
        ->entityCondition('entity_type', 'node')
        ->entityCondition('bundle', 'classified')
        ->count();
      if (!$admin_access) {
        $q
          ->propertyCondition('status', 1);
      }
      $count = $q
        ->execute();
      $params = array(
        '!link' => l($term->name, classified_term_path($term)),
        '!description' => filter_xss_admin($term->description),
      );
      $category = empty($term->description) ? $params['!link'] : t('!link - !description', $params);
      $rows[] = array(
        theme('indentation', array(
          'size' => $term->depth,
        )) . $category,
        array(
          'class' => 'classified-number',
          'data' => $count,
        ),
      );
      $total += $count;
    }
    if (!$admin_access) {

      // One minute minimum lifetime.
      cache_set($cid, array(
        $header,
        $rows,
        $total,
      ), 'cache', REQUEST_TIME + 60);
    }
  }
  $link = url('node/add/classified');
  $caption = user_access('create classified content') ? format_plural($total, 'Only one Classified Ad. <a href="!link">Add one</a>', '@count Classified Ads. <a href="!link">Add one</a>', array(
    '!link' => $link,
  )) : format_plural($total, 'Only one Classified Ad.', '@count Classified Ads');
  $ret = array(
    'table' => array(
      '#theme' => 'table',
      '#header' => $header,
      '#rows' => $rows,
      '#attributes' => array(
        'class' => array(
          'classified-term-list',
        ),
      ),
      '#caption' => $caption,
    ),
  );
  return $ret;
}

/**
 * Page callback for classified/<tid>.
 *
 * Unlike the standard taxonomy term page, this one only accepts one term from
 * the module vocabulary, and builds a breadcrumb trail based on the tree
 * assumption for that vocabulary. It does NOT honor the sticky flag,
 * the expiration date taking priority over it.
 *
 * @param object $term
 *   A fully loaded term used as a Classified Ads category to list.
 *
 * @return array
 *   Render array for the ads listing.
 *
 * @see classified_term_load()
 */
function _classified_page_term($term) {
  $bc = _classified_get_breadcrumb_by_term($term, FALSE);
  drupal_set_breadcrumb($bc);
  unset($bc);
  $ret = array();
  $children = taxonomy_get_children($term->tid);
  $children_list = array();
  foreach ($children as $child_tid => $child_term) {
    $q = new EntityFieldQuery();
    $count = $q
      ->entityCondition('entity_type', 'node', '=')
      ->entityCondition('bundle', 'classified')
      ->fieldCondition('classified_category', 'tid', $child_tid, '=', 0)
      ->count()
      ->execute();
    $children_list[] = t('!link (@count)', array(
      '!link' => l($child_term->name, classified_term_path($child_term)),
      '@count' => $count,
    ));
  }
  unset($child_tid, $child_term, $children, $count, $q);
  if (!empty($children_list)) {
    $ret['children'] = array(
      '#markup' => t('<p>Sub-categories: !cats<p>', array(
        '!cats' => implode(' ', $children_list),
      )),
    );
  }

  /** @var SelectQuery $q */
  $q = db_select('node', 'n');
  $q
    ->comment(__FUNCTION__);

  /** @var PagerDefault $q */
  $q = $q
    ->extend('PagerDefault');
  $q
    ->limit(10);
  $cn = $q
    ->innerJoin('classified_node', 'cn', 'n.vid = cn.vid');
  $ti = $q
    ->innerJoin('taxonomy_index', 'ti', 'n.nid = ti.nid');
  $q
    ->addField('n', 'nid', 'id');
  $q
    ->condition('n.type', 'classified')
    ->condition("{$ti}.tid", $term->tid)
    ->orderBy("{$cn}.expires")
    ->orderBy("n.changed")
    ->orderBy('n.created')
    ->addTag('node_access');
  $results = $q
    ->execute();
  unset($cn, $q);
  $ret['list'] = _classified_list_nodes($results);
  $ret['pager'] = array(
    '#theme' => 'pager',
  );
  return $ret;
}

/**
 * Page callback for user/<uid>/classified.
 *
 * Cannot use a traditional query because body is now a field, which can be
 * missing or renamed, and cannot use an EntityFieldQuery because we need to
 * sort on the secondary table of the node type, whereas propertyOrderBy only
 * works on the base table, in this case {node}.
 *
 * So we do a poor man's EFQ to get the node_ids, and leave it to the list
 * function to load nodes as needed.
 *
 * @param object $account
 *   A user account for which to list authored ads.
 *
 * @return array
 *   Render array for the ads list.
 */
function _classified_page_user_ads($account) {
  global $user;
  $min_status = $account->uid == $user->uid || user_access('administer nodes') ? 0 : 1;

  /** @var SelectQuery $q */
  $q = db_select('node', 'n');
  $q
    ->comment(__FUNCTION__);

  /** @var PagerDefault $q */
  $q = $q
    ->extend('PagerDefault');
  $q
    ->limit(10);
  $cn = $q
    ->innerJoin('classified_node', 'cn', 'n.vid = cn.vid');
  $q
    ->addField('n', 'nid', 'id');
  $q
    ->condition('n.type', 'classified')
    ->condition('n.uid', $account->uid)
    ->condition('n.status', $min_status, '>=')
    ->orderBy("{$cn}.expires")
    ->orderBy("n.changed")
    ->orderBy('n.created')
    ->addTag('node_access');
  $results = $q
    ->execute();
  unset($cn, $min_status, $q);
  $ret['list'] = _classified_list_nodes($results);
  $ret['pager'] = array(
    '#theme' => 'pager',
  );
  return $ret;
}

/**
 * Convert node to a database-compatible format.
 *
 * Specifically, convert the date array to a timestamp.
 * Nothing to return: changes are applied to the passed node.
 *
 * @param object $node
 *   A Classified Ad node on the verge of being saved.
 *
 * @see classified_insert()
 * @see classified_update()
 */
function _classified_presave($node) {
  if (!isset($node->expires)) {
    $node->expires = 0;
  }
  if (is_array($node->expires)) {
    $node->expires = _classified_get_timestamp_from_date($node->expires);
  }
  elseif (is_int($node->expires) && $node->expires > 0) {

    // Allows timestamps and pass them straight.
  }
  else {

    // Set to non-expiring.
    $node->expires = -1;
    watchdog('classified', 'Invalid "expires" field on node @nid: @expires', array(
      '@nid' => $node->nid,
      '@expires' => var_export($node->expires, TRUE),
    ), WATCHDOG_WARNING, 'node/' . $node->nid);
  }
}

/**
 * Re-implement taxonomy_node_get_terms_by_vocabulary(), removed in Drupal 7.
 *
 * @param object $node
 *   A Classified Ad node containing at least a nid field.
 * @param int $vid
 *   The id of the vocabulary by which to filter terms.
 *
 * @return array
 *   An array of taxonomy terms for the node, limited to the passed vocabulary.
 *
 * @see _classified_get_breadcrumb_by_node()
 */
function _classified_taxonomy_node_get_terms_by_vocabulary($node, $vid) {

  /** @var SelectQuery $q */
  $q = db_select('taxonomy_index', 'ti')
    ->comment(__FUNCTION__);
  $q
    ->join('taxonomy_term_data', 'td', 'ti.tid = td.tid');
  $q
    ->join('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid');
  $q
    ->fields('ti', array(
    'tid',
  ))
    ->addTag('node_access')
    ->condition('ti.nid', $node->nid)
    ->condition('td.vid', $vid);
  $term_ids = $q
    ->execute()
    ->fetchCol();
  $terms = taxonomy_term_load_multiple($term_ids);
  return $terms;
}

/**
 * Implements hook_block_configure().
 */
function classified_block_configure($delta) {
  if (!in_array($delta, array(
    'recent',
    'popular',
  ))) {
    $ret = NULL;
  }
  else {
    $ret = array();
    $name = $delta . '-count';
    $ret['classified-' . $name] = array(
      '#type' => 'textfield',
      '#title' => t('Number of entries'),
      '#default_value' => _classified_get($name),
      '#description' => t('Define the maximum number of ads displayed in the block. Erase for default value.'),
      '#size' => 3,
      '#maxlength' => 3,
    );
  }
  return $ret;
}

/**
 * Implements hook_block_info().
 */
function classified_block_info() {
  $ret = array(
    'recent' => array(
      'info' => t('Recent ads'),
      'cache' => DRUPAL_CACHE_GLOBAL,
    ),
    'popular' => array(
      'info' => t('Popular ads'),
      'cache' => DRUPAL_CACHE_GLOBAL,
    ),
    'stats' => array(
      'info' => t('Ad Stats'),
      'cache' => DRUPAL_CACHE_PER_ROLE,
    ),
  );
  foreach ($ret as $delta => $block) {
    $ret[$delta]['info'] = t('Classified - @info', array(
      '@info' => $ret[$delta]['info'],
    ));
  }
  return $ret;
}

/**
 * Implements hook_block_save().
 */
function classified_block_save($delta, $edit) {
  if (in_array($delta, array(
    'recent',
    'popular',
  ))) {
    $vars = _classified_get_vars();
    $name = 'classified-' . $delta . '-count';
    $count = $edit[$name];
    if (!is_numeric($count) || $count < 0 || $count > 999) {
      drupal_set_message(t('Invalid number of entries requested (@count). Resetting to default (@default).', array(
        '@count' => empty($count) ? 0 : $count,
        '@default' => $vars[$name],
      )));
      $count = $vars[$name];
    }
    variable_set($name, $count);
  }
}

/**
 * Implements hook_block_view().
 */
function classified_block_view($delta) {
  $function = '_classified_block_view_' . $delta;
  $ret = function_exists($function) ? $function() : NULL;
  return $ret;
}

/**
 * Implements hook_context_plugins().
 */
function classified_context_plugins() {
  $path = drupal_get_path('module', 'classified') . '/plugins';
  $plugins = array();
  $plugins['classified_context_condition_classified'] = array(
    'handler' => array(
      'path' => $path,
      'file' => 'classified_context_condition_path.inc',
      'class' => 'classified_context_condition_path',
      'parent' => 'context_condition_path',
    ),
  );
  return $plugins;
}

/**
 * Implements hook_context_registry_alter().
 *
 * Override the default path condition plugin with our own.
 */
function classified_context_registry_alter(&$registry) {
  if (!empty($registry['conditions']['path'])) {
    $registry['conditions']['path']['plugin'] = 'classified_context_condition_classified';
  }
}

/**
 * Implements hook_cron().
 *
 * Order of operations is important: purge have the highest priority, then
 * expiration, which should only happen on non-already-purged node, then
 * notifications, from the closest to deletion to the farthest one.
 */
function classified_cron($time = NULL) {

  // Skip invalid values of time like those submitted by Ultimate Cron.
  // @link https://www.drupal.org/node/2468505
  if (!is_int($time)) {
    $time = NULL;
  }
  module_load_include('inc', 'classified', 'classified.scheduled');
  $time = _classified_get_time($time);
  _classified_scheduled_build_purge($time);
  _classified_scheduled_build_expire($time);
  foreach (_classified_get_notify_kinds() as $kind) {
    _classified_scheduled_build_notify($kind, $time);
  }
}

/**
 * Implements hook_ctools_plugin_api().
 */
function classified_ctools_plugin_api($module, $api) {
  if ($module == 'context' && $api == 'plugins') {
    return array(
      'version' => 3,
    );
  }
  else {
    return NULL;
  }
}

/**
 * Implements hook_ctools_plugin_directory().
 */
function classified_ctools_plugin_directory($module, $plugin) {
  if ($module == 'ctools' && $plugin == 'content_types') {
    $ret = 'plugins/' . $plugin;
  }
  else {
    $ret = NULL;
  }
  return $ret;
}

/**
 * Implements hook_delete().
 */
function classified_delete($node) {
  $ret = db_delete('classified_node')
    ->condition('nid', $node->nid)
    ->execute();

  // PDO errors return NULL, not FALSE on error.
  if ($ret === NULL) {
    watchdog('classified', 'Error deleting node @nid: @title', array(
      '@nid' => $node->nid,
      '@title' => $node->title,
    ), WATCHDOG_ERROR, l($node->title, 'node/' . $node->nid));
  }
  else {
    cache_clear_all('classified:overview', 'cache');
  }
}

/**
 * Implements hook_entity_info_alter().
 *
 * - add the Classified Ad view mode (classified_content_build_modes() on D6)
 */
function classified_entity_info_alter(&$entity_info) {
  $entity_info['node']['view modes']['classified'] = array(
    // Was 'ad list' / t('Ad list') in Views 6.x.
    'label' => t('Classified Ad'),
    'custom settings' => TRUE,
  );
}

/**
 * Implements hook_field_extra_fields().
 *
 * Allow repositioning of the expires box.
 */
function classified_field_extra_fields() {
  $extra['node']['classified'] = array(
    'form' => array(
      'expires_fs' => array(
        'label' => t('Ad Expiration'),
        'description' => NULL,
        'weight' => 0,
      ),
    ),
    'display' => array(
      'expires' => array(
        'label' => t('Ad expiration'),
        'description' => t('Publication limit for ad'),
        'weight' => 2,
      ),
    ),
  );
  return $extra;
}

/**
 * Implements hook_field_formatter_info().
 *
 * - Classified Ads term reference formatter links to Classified Ads pages, not
 *   standard taxonomy pages.
 */
function classified_field_formatter_info() {
  $ret = array();
  $ret['taxonomy_term_reference_classified_link'] = array(
    'label' => t('Classified Ads link'),
    'description' => t('A link to the Classified Ads per-category listing for ad categories, or to default taxonomy pages otherwise'),
    'field types' => array(
      'taxonomy_term_reference',
    ),
    'settings' => array(
      'link_title' => FALSE,
    ),
  );
  return $ret;
}

/**
 * Implements hook_field_formatter_settings_form().
 *
 * - should Classified Ads formatter add a "title" attribute ?
 *
 * We do not test the type of formatter since we only defined one.
 */
function classified_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $ret = array();
  $ret['link_title'] = array(
    '#type' => 'checkbox',
    '#title' => t('With link title'),
    '#default_value' => $settings['link_title'],
  );
  return $ret;
}

/**
 * Implements hook_field_formatter_settings_summary().
 *
 * We do not test the formatter type since we only defined one.
 */
function classified_field_formatter_settings_summary($field, $instance, $view_mode) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $ret = $settings['link_title'] ? t('With link title') : t('Without link title');
  return $ret;
}

/**
 * Implements hook_field_formatter_view().
 *
 * An ad may appear in multiple categories. Link will to Classified Ads pages
 * only for terms in the Classified Ads vocabulary, and normal taxonomy pages
 * otherwise.
 *
 * We do not test the formatter type since we only defined one.
 */
function classified_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $ret = array();
  $vid = _classified_get('vid');
  $term_ids = array();
  foreach ($items as $item) {
    $term_ids[] = $item['tid'];
  }
  $terms = taxonomy_term_load_multiple($term_ids);
  foreach ($items as $delta => $item) {
    $tid = $item['tid'];
    if ($display['settings']['link_title']) {
      $title = $terms[$tid]->vid == $vid ? t('Classified Ads in the @title category', array(
        '@title' => $terms[$tid]->name,
      )) : t('Content flagged with @title', array(
        '@title' => $terms[$tid]->name,
      ));
      $attributes = array(
        'title' => $title,
      );
    }
    else {
      $attributes = array();
    }
    $ret[$delta] = array(
      '#markup' => l($terms[$tid]->name, classified_term_path($terms[$tid]), array(
        'attributes' => $attributes,
      )),
    );
  }
  return $ret;
}

/**
 * Implements hook_form().
 *
 * - Insert title, body.
 * - Insert expiration date depending on admin permission.
 */
function classified_form($node, &$form_state) {

  // Needed for body size limit and date display.
  drupal_add_js(drupal_get_path('module', 'classified') . '/classified.js');
  drupal_add_js(array(
    'classified' => array(
      'max_length' => _classified_get('max-length'),
    ),
  ), 'setting');
  $type = node_type_get_type($node);
  $form = array();
  if (!empty($type->has_title)) {
    $form['title'] = array(
      '#type' => 'textfield',
      '#title' => empty($type->title_label) ? '' : check_plain($type->title_label),
      '#required' => TRUE,
      '#default_value' => $node->title,
      '#weight' => -5,
    );
  }

  // Can only happen when creating a new node.
  if (empty($node->expires)) {
    $lifetimes = _classified_get('lifetimes');
    $node->expires = REQUEST_TIME + reset($lifetimes) * 24 * 60 * 60;
    unset($lifetimes);
  }

  // The form can be submitted for a field (image) instead of the whole form,
  // in which case the expire_date|mode keys will not be set, so testing for
  // "submitted" is useless.
  $date = isset($form_state['values']['expire_date']) ? $form_state['values']['expire_date'] : _classified_get_date_from_timestamp($node->expires);
  $mode = isset($form_state['values']['expire_mode']) ? $form_state['values']['expire_mode'] : NULL;
  $form['expires'] = array(
    '#type' => 'value',
    '#value' => $node->expires,
  );

  // Placeholder for submit-type data from expire_date.
  $form['expire_date_ts'] = $form['expires'];

  // Expiration fieldset: choices depend on permissions.
  $is_update = isset($node->nid);
  $is_privileged = user_access('administer classified ads') || user_access('reset classified ads expiration');
  $form['expires_fs'] = array(
    '#type' => 'fieldset',
    '#title' => t('Classified Ad expiration'),
    '#collapsible' => TRUE,
    '#collapsed' => !$is_privileged,
  );

  // Available expiration modes depend on node existence and user permissions.
  $existing_modes = array(
    'node' => t('Keep current expiration date'),
    'reset' => t('Define expiration date automatically, based on the ad category.'),
    'force' => t('Define expiration date manually'),
  );
  $modes = array();

  // Only display expire info for existing nodes.
  if ($is_update) {
    $form['expires_fs']['#description'] = '<p>' . t('This ad is currently due to expire on @expire.', array(
      '@expire' => strftime(_classified_get('date-format'), _classified_get_timestamp_from_date($date)),
    )) . '</p>';

    // Can only keep info if it already exists.
    $modes['node'] = $existing_modes['node'];
  }

  // Default mode is always available.
  $modes['reset'] = $existing_modes['reset'];
  if ($is_privileged) {
    $modes['force'] = $existing_modes['force'];
  }
  if (is_null($mode)) {
    $keys = array_keys($modes);
    $mode = reset($keys);
    unset($keys);
  }
  $form['expires_fs']['expire_mode'] = array(
    '#type' => 'radios',
    '#options' => $modes,
    '#default_value' => $mode,
    '#weight' => 0,
  );
  $form['expires_fs']['expire_date'] = array(
    '#type' => 'date',
    '#title' => t('Ad expiration date'),
    '#default_value' => $date,
    '#description' => t('This field will only be applied if the date is defined manually.'),
    '#weight' => 1,
    '#access' => $is_privileged,
  );

  // Normalize date to timestamp.
  $form['#submit'][] = 'classified_form_submit';
  return $form;
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Allow access to the options fieldset on classified node forms to Classified
 * Ads administrators.
 */
function classified_form_classified_node_form_alter(&$form, $form_state) {
  $form['options']['#access'] = user_access('administer classified ads');
}

/**
 * Submit handler for node form: normalize node form values.
 */
function classified_form_submit(&$form, &$form_state) {
  $form_state['values']['expire_date_ts'] = _classified_get_timestamp_from_date($form_state['values']['expire_date']);
}

/**
 * Implements hook_help().
 */
function classified_help($section, $arg) {
  switch ($section) {
    case 'admin/help#classified':
      $ret = t('<p>The Classified Ads modules allows users to create ads with an automatic expiration period.</p><p>If the optional classified_notifications module is enabled, warnings will be send at various points in the ad lifetime:</p><ul><li>when they reach half their scheduled lifetime</li><li>one day before they expire</li><li>upon their expiration</li><li>one day before they are deleted</li><li>and upon their deletion.</li></ul>') . t('<p>This module is interfaced with CCK and Views: it exposes a specific build mode, enabling node-based Views to look like those provided by the module itself by using the "Ad list" build mode in node style; and it exposes the ad expiration date.</p>') . t('<p>If you install the Advanced Help module, you will find a good deal of additional help, notably about theming, use of the Classified Ads API, and integration of Classified Ads with other modules like Context, Panels, Token, and Views');
      break;
    case 'admin/content/node-type/classified/display/classified':
      $ret = t('<p>This build mode appears as "Ad list" when building node Views in "node" style.</p>');
      break;
    default:
      $ret = NULL;
  }
  return $ret;
}

/**
 * Implements hook_insert().
 */
function classified_insert($node) {
  $ret = drupal_write_record('classified_node', $node);
  if ($ret === FALSE) {
    watchdog('classified', 'Error inserting ad @title', array(
      '@title' => $node->title,
    ), WATCHDOG_ERROR);
  }
  else {
    cache_clear_all('classified:overview', 'cache');
  }
}

/**
 * Implements hook_load().
 */
function classified_load($nodes) {
  if (!empty($nodes)) {
    $vocabulary_ids = array();
    foreach ($nodes as $node) {
      $vocabulary_ids[] = $node->vid;
    }

    /** @var SelectQuery $q */
    $q = db_select('classified_node', 'cn')
      ->fields('cn', array(
      'nid',
      'vid',
      'expires',
      'notify',
    ))
      ->condition('cn.vid', $vocabulary_ids, 'IN');
    $results = $q
      ->execute();
    foreach ($results as $result) {
      $nodes[$result->nid]->expires = $result->expires;
      $nodes[$result->nid]->notify = $result->notify;
    }
  }
}

/**
 * Implements hook_menu().
 */
function classified_menu() {
  $items = array();
  $items['admin/config/content/classified'] = array(
    'title' => 'Classified Ads',
    'description' => 'Configure ad size, lifetime, grace period, and format in lists.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'classified_admin_settings',
    ),
    'file' => 'classified.admin.inc',
    'access arguments' => array(
      'administer classified ads',
    ),
  );
  $items['classified'] = array(
    'page callback' => '_classified_page_overview',
    'title' => 'Classified Ads',
    'access arguments' => array(
      'access content',
    ),
  );
  $items['classified/%classified_term'] = array(
    'page callback' => '_classified_page_term',
    'page arguments' => array(
      1,
    ),
    'title callback' => 'filter_admin_format_title',
    'title arguments' => array(
      1,
    ),
    'access arguments' => array(
      'access content',
    ),
  );

  /* classified/scheduled/* are safe operations, which can be run anytime by
   * anyone: they will only run if the time is appropriate, and an early run
   * will reduce load on the scheduled run, so it is actually beneficial
   */
  $items['classified/scheduled'] = array(
    'type' => MENU_CALLBACK,
    'page callback' => 'drupal_goto',
    'page arguments' => array(
      'classified',
    ),
    // See detailed comment above.
    'access callback' => TRUE,
  );
  $items['classified/scheduled/purge'] = array(
    'type' => MENU_CALLBACK,
    'page callback' => '_classified_scheduled_page_purge',
    // Triggering purges by cron is simpler if allowed to any client.
    'access callback' => TRUE,
    'file' => 'classified.scheduled.inc',
  );
  $items['classified/scheduled/expire'] = array(
    'type' => MENU_CALLBACK,
    'page callback' => '_classified_scheduled_page_expire',
    // Triggering expirations by cron is simpler if allowed to any client.
    'access callback' => TRUE,
    'file' => 'classified.scheduled.inc',
  );
  $items['classified/scheduled/notify/%classified_notify_kind'] = array(
    'type' => MENU_CALLBACK,
    'page callback' => '_classified_scheduled_page_notify',
    'page arguments' => array(
      3,
    ),
    // Triggering notifications by cron is simpler if allowed to any client.
    'access callback' => TRUE,
    'file' => 'classified.scheduled.inc',
  );
  $items['user/%user/classified'] = array(
    'title' => 'Ads',
    'type' => MENU_LOCAL_TASK,
    'page callback' => '_classified_page_user_ads',
    'page arguments' => array(
      1,
    ),
    'access callback' => 'user_view_access',
    'access arguments' => array(
      1,
    ),
  );
  return $items;
}

/**
 * Implements hook_node_access().
 */
function classified_node_access($node, $op, $account) {

  // $node->type on $op == create, $node otherwise.
  $type = is_string($node) ? $node : $node->type;

  // Ads admin bypasses checks.
  if ($type == 'classified' && user_access('administer classified ads', $account)) {
    $ret = NODE_ACCESS_ALLOW;
  }
  else {
    $ret = NODE_ACCESS_IGNORE;
  }
  return $ret;
}

/**
 * Implements hook_node_info().
 *
 * @todo XXX 2011-08-04 FGM: note in D6 version to check
 *   "match nodeapi $node->type for spam module to add spam reporting links"
 */
function classified_node_info() {
  $ret = array(
    'classified' => array(
      // Cannot call node_get_types() since it ends up calling this code.
      'name' => t('Classified Ad'),
      'base' => 'classified',
      'description' => t('Contains a title, a body, and an administrator-defined expiration date'),
      'has_title' => TRUE,
      'title_label' => t('Ad Title'),
      'locked' => TRUE,
    ),
  );
  return $ret;
}

/**
 * Implements hook_node_presave().
 *
 * Auto-assign expiration date before saving, for both update and insert.
 *
 * This used to trigger modr8 in versions <= Drupal 6. However modr8 is not
 * ported to Drupal 7 and the node.moderate column no longer exists in core.
 * Use an actual workflow solution instead.
 */
function classified_node_presave($node) {
  if ($node->type != 'classified') {
    return;
  }
  if (!isset($node->expire_mode)) {
    $node->expire_mode = 'reset';
  }
  $expires_before = isset($node->expires) ? $node->expires : PHP_INT_MAX;
  $now = REQUEST_TIME;
  switch ($node->expire_mode) {
    case 'force':
      $node->expires = $node->expire_date_ts;
      break;
    case 'node':

      // Do nothing: keep unchanged.
      break;
    case 'reset':
    default:
      $lifetimes = _classified_get('lifetimes');
      $category = isset($node->classified_category[LANGUAGE_NONE][0]['tid']) ? $node->classified_category[LANGUAGE_NONE][0]['tid'] : NULL;
      $days = empty($category) || empty($lifetimes[$category]) ? reset($lifetimes) : $lifetimes[$category];
      $node->expires = empty($category) || empty($lifetimes[$category]) ? $now + $days * 24 * 60 * 60 : $now + $days * 24 * 60 * 60;
      break;
  }

  // Republish ads if their expiration date is in the future.
  if ($expires_before < $now && !$node->status && $node->expires > $now) {
    $node->status = 1;
  }
  unset($node->expire_date);
  unset($node->expire_date_ts);
  unset($node->expire_mode);
}

/**
 * Implements hook_node_revision_delete().
 */
function classified_node_revision_delete($node) {
  if ($node->type != 'classified') {
    return;
  }
  $ret = db_delete('classified_node')
    ->condition('vid', $node->vid)
    ->execute();

  // PDO errors return NULL, not FALSE on error.
  if (!isset($ret)) {
    watchdog('classified', 'Error deleting revision @vid of node @nid', array(
      '@vid' => $node->vid,
      '@nid' => $node->nid,
    ), WATCHDOG_WARNING, 'node/' . $node->nid);
  }
}

/**
 * Implements hook_node_type_update().
 *
 * Just log the event; nothing to do:
 * - machine name and content type are locked by classified_node_info(),
 * - other node type properties are not used by the module,
 * - node updates are conducted by node.module as needed.
 */
function classified_node_type_update($info) {
  $type = $info->orig_type;
  if ($type == 'classified') {
    watchdog('classified', 'Classified node type %type has been modified: %changes', array(
      '%type' => $type,
      '%changes' => var_export($info, TRUE),
    ), WATCHDOG_NOTICE, l(t('Edit'), "admin/structure/types/manage/{$type}"));
  }
}

/**
 * Menu loader for check notify kind.
 *
 * @param int $kind
 *   One of the check kinds enumerated in _classified_get_notify_kinds().
 *
 * @return string|false
 *   Is the kind of check allowed ?
 */
function classified_notify_kind_load($kind) {
  if (in_array($kind, _classified_get_notify_kinds())) {
    return $kind;
  }
  else {
    return FALSE;
  }
}

/**
 * Implements hook_page_build().
 */
function classified_page_build(&$page) {
  $page['content']['#attached']['css'][] = drupal_get_path('module', 'classified') . '/theme/classified.css';
}

/**
 * Implements hook_permission().
 *
 * Standard node permissions are generated by node.module on Drupal 7.
 */
function classified_permission() {
  $ret = array(
    'administer classified ads' => array(
      'title' => t('administer classified ads'),
      'description' => t("Administer the Ads system. Does not include full node access to Ad nodes. Use 'administer nodes' for that"),
    ),
    'reset classified ads expiration' => array(
      'title' => t('reset classified ads expiration'),
    ),
  );
  return $ret;
}

/**
 * Implements hook_preprocess_classified_expires().
 *
 * Define four formats for the expiration date of a node:
 * - expires: default date format for the module,
 * - expires_raw: UNIX timestamp,
 * - remaining: days to expiration,
 * - remaining_ratio: the percentile of ad lifetime already expired.
 */
function classified_preprocess_classified_expires(&$variables) {
  $node = $variables['node'];
  $now = REQUEST_TIME;
  $expires_raw = $node->expires;
  $variables['expires_raw'] = $expires_raw;
  $date_format = _classified_get('date-format');
  $variables['expires'] = strftime($date_format, $expires_raw);
  $variables['remaining'] = format_interval($expires_raw - $now, 1);
  $remaining_ratio = $expires_raw > $now ? round(100 * (($expires_raw - $now) / ($expires_raw - $variables['node']->created))) : 0;
  $variables['remaining_ratio'] = $remaining_ratio;

  // Color-code ratios.
  if ($remaining_ratio == 0) {
    $class = 'classified-expires-expired';
  }
  elseif ($remaining_ratio < 20) {
    $class = 'classified-expires-soon';
  }
  else {
    $class = 'classified-expires-later';
  }
  $variables['remaining_class'] = $class;
}

/**
 * Used to invalidate our cache when anything changes within our vocabulary.
 *
 * See classified_taxonomy_(term|vocabulary)_(delete|insert|update)
 */
function classified_taxonomy($vid) {
  $classified_vid = _classified_get('vid');
  if ($classified_vid == $vid) {
    cache_clear_all('classified:overview', 'cache');
  }
}

/**
 * Implements hook_taxonomy_term_delete().
 */
function classified_taxonomy_term_delete($term) {
  classified_taxonomy($term->vid);
}

/**
 * Implements hook_taxonomy_term_insert().
 */
function classified_taxonomy_term_insert($term) {
  classified_taxonomy($term->vid);
}

/**
 * Implements hook_taxonomy_term_update().
 */
function classified_taxonomy_term_update($term) {
  classified_taxonomy($term->vid);
}

/**
 * Implements hook_taxonomy_vocabulary_delete().
 */
function classified_taxonomy_vocabulary_delete($vocabulary) {
  classified_taxonomy($vocabulary->vid);
}

/**
 * Implements hook_taxonomy_vocabulary_insert().
 */
function classified_taxonomy_vocabulary_insert($vocabulary) {
  classified_taxonomy($vocabulary->vid);
}

/**
 * Implements hook_taxonomy_vocabulary_update().
 */
function classified_taxonomy_vocabulary_update($vocabulary) {
  classified_taxonomy($vocabulary->vid);
}

/**
 * Menu loader for %classified_term.
 *
 * Static cache implemented because loader appears to be often invoked twice: at
 * loading, and during page preprocess, for theme('help').
 *
 * @param int $tid
 *   The id for a term expected to be a Classified Ads category.
 *
 * @return object
 *   A fully loaded term object if the tid is for a valid ads category.
 */
function classified_term_load($tid) {
  static $terms = array();
  if (!isset($terms[$tid])) {
    $term = taxonomy_term_load($tid);
    $vid = _classified_get('vid');
    $terms[$tid] = isset($term->vid) && $term->vid == $vid ? $term : FALSE;
  }
  $ret = $terms[$tid];
  return $ret;
}

/**
 * Implements the former D6 hook_term_path().
 *
 * In D7 this is no longer a hook, but functions invoke it directly to obtain
 * a Classified Ads taxonomy path for terms in the Classified Ads vocabulary,
 * and normal taxonomy pages otherwise.
 */
function classified_term_path($term) {
  $vid = _classified_get('vid');
  $ret = $term->vid == $vid ? 'classified/' . $term->tid : 'taxonomy/term/' . $term->tid;
  return $ret;
}

/**
 * Implements hook_theme().
 *
 * Note: _classified_block_view_(popular|recent|stats) introduce extra hooks.
 */
function classified_theme($existing, $type, $theme, $path) {
  $ret = array(
    'classified_expires' => array(
      'variables' => array(
        'node' => NULL,
      ),
      'template' => 'classified-expires',
      'path' => $path . '/theme',
    ),
    'classified_admin_lifetimes' => array(
      'render element' => 'form',
    ),
  );
  return $ret;
}

/**
 * Implements hook_trigger_info().
 */
function classified_trigger_info() {
  return array(
    'node' => array(
      'classified_expires' => array(
        'label' => t('When a Classified Ad expires.'),
      ),
    ),
  );
}

/**
 * Implements hook_update().
 */
function classified_update($node) {
  if (!empty($node->revision)) {
    classified_insert($node);
  }
  else {
    $ret = drupal_write_record('classified_node', $node, array(
      'nid',
      'vid',
    ));
    if ($ret === FALSE) {
      watchdog('classified', 'Error updating ad @nid: "@title"', array(
        '@nid' => $node->nid,
        '@title' => $node->title,
      ), WATCHDOG_ERROR, l($node->title, 'node/' . $node->nid));
    }
    else {
      cache_clear_all('classified:overview', 'cache');
    }
  }
}

/**
 * Implements hook_validate().
 *
 * - accept empty expiration dates,
 * - accept dates in site-specific format only, and only after current time.
 */
function classified_validate($node, &$form) {

  // Coder bug #195054.
  $mode = $form['expires_fs']['expire_mode']['#value'];

  // Coder bug #195054.
  $date = $form['expires_fs']['expire_date']['#value'];
  $date = _classified_get_timestamp_from_date($date);

  // This can be a user error, as the max_length JS is not being included if
  // form_set_error has been used.
  $max_length = _classified_get('max-length');

  // Body may include <!--break-->, possibly wrapped with spaces (newlines),
  // which must not be included in the length count.
  $body = isset($node->body[LANGUAGE_NONE][0]['value']) ? $node->body[LANGUAGE_NONE][0]['value'] : NULL;
  $body = preg_replace('/^(.*)\\s*<!--break-->\\s*(.*)$/sU', '$1$2', $body);

  // Ignore \r, only count \n.
  $body = str_replace("\r\n", "\n", $body);
  $body_length = drupal_strlen($body);
  if ($max_length > 0 && $body_length > $max_length) {
    form_set_error('body', t('Text is longer than maximum authorized length: @body_length characters vs @max_length authorized.', array(
      '@body_length' => $body_length,
      '@max_length' => $max_length,
    )));
  }

  // This is likely a hacking attempt.
  if ($mode == 'force' && !user_access('administer classified ads') && !user_access('reset classified ads expiration')) {
    form_set_error('expire_mode', t('User is not allowed to force expiration date.'));
    global $user;
    watchdog('classified', 'Attempt by user @uid to force expiration reset on ad @nid: @title', array(
      '@uid' => $user->uid,
      '@nid' => $node->nid,
      '@title' => $node->title,
    ), WATCHDOG_WARNING, l($user->name, 'user/' . $user->uid) . l($node->title, ' node/' . $node->nid));
  }

  // This is likely just a user error.
  if ($mode == 'force' && $date < REQUEST_TIME) {
    form_set_error('expire_date', t('Invalid expiration date: before now.'));
  }
}

/**
 * Implements hook_view().
 *
 * When building breadcrumbs, assume a tree-structured vocabulary. Non-tree DAGs
 * with multiple parents per term are not supported.
 *
 * Only build a breadcrumbs trail if terms are defined (they should be).
 */
function classified_view($node, $view_mode) {
  if ($view_mode == 'full' && node_is_page($node)) {
    drupal_set_breadcrumb(_classified_get_breadcrumb_by_node($node));
  }
  $node->content['expires'] = array(
    '#markup' => theme('classified_expires', array(
      'node' => $node,
    )),
  );
  return $node;
}

/**
 * Implements hook_views_api().
 */
function classified_views_api() {
  $ret = array(
    'api' => 3,
    'path' => drupal_get_path('module', 'classified') . '/views',
  );
  return $ret;
}

/**
 * Implements hook_url_outbound_alter().
 *
 * When reporting broken taxonomy links, we want stack [2], because:
 * - [0] is module.inc#drupal_alter() calling classified_url_outbound_alter()
 * - [1] is common.inc#url() calling drupal_alter('url_outbound')
 */
function classified_url_outbound_alter(&$path, &$options, $original_path) {
  if (preg_match('!^taxonomy/term/(\\d+)$!', $path, $matches)) {
    $term = isset($options['entity']->vocabulary_machine_name) ? $options['entity'] : taxonomy_term_load($matches[1]);
    if (!$term) {
      $stack = debug_backtrace();
      watchdog('classified', 'Outbound taxonomy URL to nonexistent term: %path, from %emitter', array(
        '%path' => $path,
        '%emitter' => print_r($stack[2], TRUE),
      ), WATCHDOG_NOTICE);
    }
    elseif (isset($term->vocabulary_machine_name) && $term->vocabulary_machine_name == 'classified_categories') {
      $path = 'classified/' . $matches[1];
      $options['alias'] = $path;
    }
  }
}

Functions

Namesort descending Description
classified_block_configure Implements hook_block_configure().
classified_block_info Implements hook_block_info().
classified_block_save Implements hook_block_save().
classified_block_view Implements hook_block_view().
classified_context_plugins Implements hook_context_plugins().
classified_context_registry_alter Implements hook_context_registry_alter().
classified_cron Implements hook_cron().
classified_ctools_plugin_api Implements hook_ctools_plugin_api().
classified_ctools_plugin_directory Implements hook_ctools_plugin_directory().
classified_delete Implements hook_delete().
classified_entity_info_alter Implements hook_entity_info_alter().
classified_field_extra_fields Implements hook_field_extra_fields().
classified_field_formatter_info Implements hook_field_formatter_info().
classified_field_formatter_settings_form Implements hook_field_formatter_settings_form().
classified_field_formatter_settings_summary Implements hook_field_formatter_settings_summary().
classified_field_formatter_view Implements hook_field_formatter_view().
classified_form Implements hook_form().
classified_form_classified_node_form_alter Implements hook_form_FORM_ID_alter().
classified_form_submit Submit handler for node form: normalize node form values.
classified_help Implements hook_help().
classified_insert Implements hook_insert().
classified_load Implements hook_load().
classified_menu Implements hook_menu().
classified_node_access Implements hook_node_access().
classified_node_info Implements hook_node_info().
classified_node_presave Implements hook_node_presave().
classified_node_revision_delete Implements hook_node_revision_delete().
classified_node_type_update Implements hook_node_type_update().
classified_notify_kind_load Menu loader for check notify kind.
classified_page_build Implements hook_page_build().
classified_permission Implements hook_permission().
classified_preprocess_classified_expires Implements hook_preprocess_classified_expires().
classified_taxonomy Used to invalidate our cache when anything changes within our vocabulary.
classified_taxonomy_term_delete Implements hook_taxonomy_term_delete().
classified_taxonomy_term_insert Implements hook_taxonomy_term_insert().
classified_taxonomy_term_update Implements hook_taxonomy_term_update().
classified_taxonomy_vocabulary_delete Implements hook_taxonomy_vocabulary_delete().
classified_taxonomy_vocabulary_insert Implements hook_taxonomy_vocabulary_insert().
classified_taxonomy_vocabulary_update Implements hook_taxonomy_vocabulary_update().
classified_term_load Menu loader for %classified_term.
classified_term_path Implements the former D6 hook_term_path().
classified_theme Implements hook_theme().
classified_trigger_info Implements hook_trigger_info().
classified_update Implements hook_update().
classified_url_outbound_alter Implements hook_url_outbound_alter().
classified_validate Implements hook_validate().
classified_view Implements hook_view().
classified_views_api Implements hook_views_api().
_classified_block_view_popular Implements hook_block() for ('view', 'popular').
_classified_block_view_recent Implements hook_block() for ('view', 'recent').
_classified_block_view_stats Implements hook_block() for ('view', 'stats').
_classified_get A simplified alternative to variable_get().
_classified_get_breadcrumb_by_node Build a breadcrumb trail for an a Classified Ad node.
_classified_get_breadcrumb_by_term Build a Classified Ad breadcrumb trail for an a term.
_classified_get_date_from_timestamp Convert a UNIX timestamp to the format used by the date widget.
_classified_get_notify_kinds Domain definition for notification kinds.
_classified_get_timestamp_from_date Convert a UNIX timestamp from the format used by the date widget.
_classified_get_vars List the module variables.
_classified_list_nodes Prepare a node listing from a DB query resource.
_classified_page_overview Page callback for 'classified'.
_classified_page_term Page callback for classified/<tid>.
_classified_page_user_ads Page callback for user/<uid>/classified.
_classified_presave Convert node to a database-compatible format.
_classified_taxonomy_node_get_terms_by_vocabulary Re-implement taxonomy_node_get_terms_by_vocabulary(), removed in Drupal 7.