You are here

node_expire.module in Node expire 5

Alerts administrators of possibly outdated materials and optionally unpublishes them.

File

node_expire.module
View source
<?php

/**
 * @file
 * Alerts administrators of possibly outdated materials and optionally unpublishes them.
 **/

/**
 * Implementation of hook_perm().
 **/
function node_expire_perm() {
  return array(
    'administer node_expire',
    'edit expirations',
  );
}

/**
 * Implementation of hook_menu().
 **/
function node_expire_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/settings/node_expire',
      'title' => t('Node Expire'),
      'description' => t('Configure Node Expire'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'node_expire_settings_form',
      ),
      'access' => user_access('administer node_expire'),
    );
    $items[] = array(
      'path' => 'admin/settings/node_expire/settings',
      'title' => t('Settings'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'node_expire_settings_form',
      ),
      'access' => user_access('administer node_expire'),
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/settings/node_expire/defaults',
      'title' => t('Defaults'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'node_expire_default_settings_form',
      ),
      'access' => user_access('administer node_expire'),
      'type' => MENU_LOCAL_TASK,
      'weight' => 1,
    );

    // If pages are set to expire instantly, this page is of absolutely no use. Let's just hide it.
    $items[] = array(
      'path' => 'admin/content/node/outdated',
      'title' => t('Outdated Documents'),
      'callback' => 'node_expire_outdated_nodes',
      'access' => variable_get('node-expire-unpublishtime', 0) != 1 && user_access('administer nodes'),
      'type' => MENU_LOCAL_TASK,
      'weight' => 10,
    );
  }
  return $items;
}

/**
 * Implementation of hook_cron().
 **/
function node_expire_cron() {

  // Are email notifications enabled?
  if (variable_get('node-expire-enable-email', 1) == 1) {
    $updateids = array();

    /**
     * Get a list of nodes that are past the expiration threshold and also haven't been notified in the selected
     * amount of time. For the qualifying data, it gets the necessary information that a user might want to
     * include in the automatic email.
     **/
    $query = db_query("SELECT a.nid, b.title, a.expire, b.changed, c.name, c.mail FROM {node_expire} a LEFT JOIN {node} b ON " . "a.nid = b.nid LEFT JOIN {users} c ON b.uid = c.uid WHERE a.expire <= NOW() AND a.expiremode != 'none' AND a.lastnotify <= %d " . "AND b.status = 1 ORDER BY c.name ASC, b.title ASC", time() - variable_get('node-expire-renotify', 259200));
    while ($row = db_fetch_object($query)) {

      // Has this user received an out-of-date alert this run?
      if (!isset($newnotify[$row->name])) {
        $newnotify[$row->name] = array(
          'email' => $row->mail,
        );
      }

      // Has this node/book already been processed?
      $newnotify[$row->name][$row->nid] = array(
        'nid' => $row->nid,
        'title' => $row->title,
        'expire' => $row->expire,
        'changed' => $row->changed,
      );
    }

    /**
     * Now compile the messages.
     *
     * There are two lines to parse the data because we want to do a bulk mailing method rather than emailing once per expired node.
     **/
    if (count($newnotify) > 0) {

      // The subject and cc address are always the same so let's get them out of the way first.
      $subject = variable_get('node-expire-subject', '!site - Article Update Needed');
      $subject = str_replace('!site', variable_get('site_name', 'Drupal'), $subject);
      $cc = variable_get('node-expire-cc', '');

      // Go through the list of each user that will receive an alert.
      foreach ($newnotify as $tech => $contents) {

        /**
         * The e-mail address is stored in the array for easy access. We don't want to
         * count this as an expired node, so let's remove it from the data list.
         **/
        $to = $contents['email'];
        unset($contents['email']);

        // Make sure carriage returns are in UNIX format to prevent cross-OS problems.
        $body = str_replace("\r", "", variable_get('node-expire-body', "Hello !username,\r\n\r\nThe following article(s) are in " . "need for reviewing and updating. Please update these at your earliest convenience. If no changes are necessary, " . "simply open the editor and press 'Save'.\r\n\r\n!start_section!\r\nArticle: !section_article\r\nTime since " . "update: !section_timesinceupdate\r\nEdit Link: !section_editlink\r\n\r\n!stop_section!\r\n--  !site team"));

        // Replace allowed configurable variables
        $body = str_replace('!site', variable_get('site_name', 'Drupal'), $body);
        $body = str_replace('!username', $tech, $body);

        // Grab just between !start_section! and !stop_section!
        $bodysec = substr($body, strpos($body, '!start_section!') + 15, strpos($body, '!stop_section!') - strpos($body, '!start_section!') - 15);

        /**
         * We usually have !start_section!, a carriage return, and then the message for the sake of looking pretty during setup. Let's remove this
         * one instance. If an extra carriage return is requested, the user should put it at the end of the loop as the default value is.
         **/
        $bodysec = preg_replace("/^\n/", "", $bodysec);
        $newbody = '';

        // Scan each message and process it according to the template.
        foreach ($contents as $row) {
          $temp = $bodysec;
          $temp = str_replace('!section_article', $row['title'], $temp);
          $temp = str_replace('!section_timesinceupdate', format_interval(time() - $row['changed']) . ' ago', $temp);
          $temp = str_replace('!section_lastupdate', format_date($row['changed']), $temp);
          $temp = str_replace('!section_expirydate', format_date(strtotime($row['expire'])), $temp);
          $temp = str_replace('!section_unpublishdate', variable_get('node-expire-unpublishtime', 0) == 0 ? t('Never') : format_date(strtotime($row['expire']) + variable_get('node-expire-unpublishtime', 0)), $temp);
          $temp = str_replace('!section_nodelink', url('node/' . $row['nid'], null, null, true), $temp);
          $temp = str_replace('!section_editlink', url('node/' . $row['nid'] . '/edit', null, null, true), $temp);

          // Record this as sent so it's only notified once per the selected threshold.
          $updateids[] = $row['nid'];
          $newbody .= $temp;
        }

        // Now let's take out the template from the settings, and replace it with the parsed data.
        $body = substr($body, 0, strpos($body, '!start_section!')) . $newbody . substr($body, strpos($body, '!stop_section!') + 15);

        // This is just to prevent problems with "Anonymous" nodes.
        if ($to) {
          drupal_mail('notify-' . $tech, $to, $subject, $body);
        }

        // Send it to the author and the requested carbon copy address, if any.
        if ($cc) {
          drupal_mail('notify-' . $tech, $cc, $subject, $body);
        }
      }

      // Record which records were updated.
      db_query('UPDATE {node_expire} SET lastnotify = %d WHERE nid IN(%s)', time(), implode(', ', $updateids));

      // Log the event
      watchdog('node_expire', format_plural(count($updateids), 'E-mail notice submitted for node #' . $updateids[0], 'E-mail notice submitted for nodes ' . implode(', ', $updateids)), WATCHDOG_NOTICE);
    }
  }

  /**
   * We run the code to unpublish expired documents after the email communications are sent out because it only queries for published
   * documents. This way, people who want documents to instantly expire can still have email notifications sent out about them too.
   **/
  if (variable_get('node-expire-unpublishtime', 0) != 0) {

    // Find old documents.
    $unpublishme = array();
    $query = db_query("SELECT nid FROM {node_expire} WHERE expire <= '%s' AND expiremode != 'none'", date("Y-m-d H:i:s", time() - variable_get('node-expire-unpublishtime', 0)));
    while ($row = db_fetch_object($query)) {
      $unpublishme[] = $row->nid;
    }

    // If any records are to be unpublished, unpublish them, and log it through the watchdog service.
    if (count($unpublishme) > 0) {
      db_query('UPDATE {node} SET status = 0 WHERE nid IN (%s)', implode(', ', $unpublishme));
      watchdog('node_expire', format_plural(count($unpublishme), '@count node was automatically unpublished.', '@count nodes were automatically unpublished.'), WATCHDOG_NOTICE);
    }
  }
  return true;
}

/**
 * Configuration form for node_expire
 **/
function node_expire_settings_form() {

  // Publishing settings section
  $form['general'] = array(
    '#title' => t('General Settings'),
    '#type' => 'fieldset',
    '#tree' => FALSE,
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );
  $expireperiod = drupal_map_assoc(array(
    0,
    1,
    86400,
    172800,
    259200,
    345600,
    432000,
    518400,
    604800,
    1209600,
    1814400,
    2419200,
    3024000,
    3628800,
    4233600,
    4838400,
    5443200,
    6048000,
    6652800,
    7257600,
    7862400,
    8467200,
    9072000,
    9676800,
    10281600,
    10886400,
  ), 'format_interval');
  $expireperiod[0] = t('Never');
  $expireperiod[1] = t('Instantly');
  $form['general']['unpublishtime'] = array(
    '#title' => t('Timeout for automatic unpublishing'),
    '#description' => t('The duration of time when a node is expired for it to become automatically unpublished. Notice: ' . 'Unpublished documents won\'t trigger any expiration notices.'),
    '#type' => 'select',
    '#options' => $expireperiod,
    '#default_value' => variable_get('node-expire-unpublishtime', 0),
  );

  // Only allow inheritance for book pages.
  if (module_exists('book')) {
    $form['general']['book_inherit'] = array(
      '#title' => t('Book pages - Inheritance'),
      '#description' => t('Enable inheritance/propagation of expiration times amongst child books.'),
      '#type' => 'select',
      '#options' => array(
        1 => 'Yes',
        0 => 'No',
      ),
      '#default_value' => variable_get('node-expire-book-props', 1),
    );
  }

  // Notification settings section
  $form['notify'] = array(
    '#title' => t('Notifications'),
    '#type' => 'fieldset',
    '#tree' => FALSE,
    '#collapsible' => TRUE,
    '#collapsed' => variable_get('node-expire-enable-email', 1) == 0,
  );
  $form['notify']['enabled'] = array(
    '#title' => t('Enable email notifications'),
    '#description' => t('Whether or not to e-mail out about expired nodes.'),
    '#type' => 'select',
    '#options' => array(
      1 => 'Yes',
      0 => 'No',
    ),
    '#default_value' => variable_get('node-expire-enable-email', 1),
  );
  $period = drupal_map_assoc(array(
    86400,
    172800,
    259200,
    345600,
    432000,
    518400,
    604800,
  ), 'format_interval');
  $form['notify']['renotify'] = array(
    '#title' => t('Re-notify user every'),
    '#description' => t('The length of time before the user is renotified of old content.'),
    '#type' => 'select',
    '#options' => $period,
    '#default_value' => variable_get('node-expire-renotify', 259200),
  );
  $form['notify']['cc'] = array(
    '#title' => t('Carbon Copy Address'),
    '#description' => t('An e-mail address to carbon copy on all notifications.'),
    '#type' => 'textfield',
    '#size' => 50,
    '#default_value' => variable_get('node-expire-cc', ''),
  );

  // E-mail content settings
  $form['emlcontent'] = array(
    '#title' => t('E-mail Content'),
    '#type' => 'fieldset',
    '#tree' => FALSE,
    '#collapsible' => TRUE,
    '#collapsed' => variable_get('node-expire-enable-email', 1) == 0,
  );
  $form['emlcontent']['subject'] = array(
    '#title' => t('Subject'),
    '#description' => t('The subject of the automated alerts. Available variables are: !site'),
    '#type' => 'textfield',
    '#size' => 60,
    '#maxlength' => 180,
    '#default_value' => variable_get('node-expire-subject', '!site - Article Update Needed'),
  );
  $form['emlcontent']['body'] = array(
    '#title' => t('Body'),
    '#description' => t('The body of the automated alerts. Available variables are: !username !start_section! !stop_section! ' . '!section_article !section_timesinceupdate !section_lastupdate !section_expirydate !section_unpublishdate !section_nodelink ' . '!section_editlink'),
    '#type' => 'textarea',
    '#rows' => 11,
    '#cols' => 60,
    '#default_value' => variable_get('node-expire-body', "Hello !username,\r\n\r\nThe following article(s) are in " . "need for reviewing and updating. Please update these at your earliest convenience. If no changes are necessary, " . "simply open the editor and press 'Save'.\r\n\r\n!start_section!\r\nArticle: !section_article\r\nTime since " . "update: !section_timesinceupdate\r\nEdit Link: !section_editlink\r\n\r\n!stop_section!\r\n--  !site team"),
  );
  return system_settings_form($form);
}

/**
 * Implementation of hook_form_validate()
 **/
function node_expire_settings_form_validate($form_id, $form_values) {

  // Only validate the form if we're saving changes. We don't care about values if we're just resetting them anyway.
  if ($form_values['op'] == t('Save configuration')) {

    // Is the CC address valid?
    if ($form_values['cc'] && !valid_email_address($form_values['cc'])) {
      form_set_error('cc', t('The entered carbon copy address is invalid.'));
    }

    // Count instances of !start_section!
    $matches = array();
    preg_match_all('/!start_section!/', $form_values['body'], $matches);
    if (count($matches[0]) > 1) {
      form_set_error('body', t('The tag "!start_section!" can only be used once.'));
    }

    // Make sure instances of !start_section! match !stop_section!
    $matches2 = array();
    preg_match_all('/!stop_section!/', $form_values['body'], $matches2);
    if (count($matches[0]) != count($matches2[0])) {
      form_set_error('body', t('The tag "!stop_section!" is missing or doesn\'t match with "!start_section!".'));
    }
  }
}

/**
 * Implementation of hook_form_submit()
 **/
function node_expire_settings_form_submit($form_id, $form_values) {

  // Do we want to reset to the defaults?
  if ($form_values['op'] == t('Reset to defaults')) {
    variable_del('node-expire-body');
    variable_del('node-expire-book-props');
    variable_del('node-expire-cc');
    variable_del('node-expire-enable-email');
    variable_del('node-expire-renotify');
    variable_del('node-expire-subject');
    variable_del('node-expire-unpublishtime');
    drupal_set_message(t('Settings reset back to defaults.'));
  }
  else {

    // Only allow inheritance for book pages.
    if (!module_exists('book')) {
      variable_del('node-expire-book-props');
    }
    else {
      variable_set('node-expire-book-props', $form_values['book_inherit']);
    }

    // Blank body resets to default
    if (!$form_values['body']) {
      variable_del('node-expire-body');
    }
    else {
      variable_set('node-expire-body', $form_values['body']);
    }

    // Blank subject resets to default
    if (!$form_values['subject']) {
      variable_del('node-expire-subject');
    }
    else {
      variable_set('node-expire-subject', $form_values['subject']);
    }

    // All other settings can be saved as-is.
    variable_set('node-expire-enable-email', $form_values['enabled']);
    variable_set('node-expire-cc', $form_values['cc']);
    variable_set('node-expire-renotify', $form_values['renotify']);
    variable_set('node-expire-unpublishtime', $form_values['unpublishtime']);
    drupal_set_message(t('Saved new settings.'));
  }
}

/**
 * Configuration form for default expirations for node_expire
 **/
function node_expire_default_settings_form() {

  // Get current settings
  $curdefaults = variable_get('node-expire-node-visibility', array());
  $period = array(
    '+1 day' => t('1 Day'),
    '+2 days' => t('2 Days'),
    '+3 days' => t('3 Days'),
    '+4 days' => t('4 Days'),
    '+5 days' => t('5 Days'),
    '+6 days' => t('6 Days'),
    '+1 week' => t('1 Week'),
    '+2 weeks' => t('2 Weeks'),
    '+3 weeks' => t('3 Weeks'),
    '+1 month' => t('1 Month'),
    '+2 months' => t('2 Months'),
    '+3 months' => t('3 Months'),
    '+4 months' => t('4 Months'),
    '+5 months' => t('5 Months'),
    '+6 months' => t('6 Months'),
    '+7 months' => t('7 Months'),
    '+8 months' => t('8 Months'),
    '+9 months' => t('9 Months'),
    '+10 months' => t('10 Months'),
    '+11 months' => t('11 Months'),
    '+1 Year' => t('1 Year'),
  );

  // Make the options available for each node type.
  $types = node_get_types();
  foreach ($types as $node) {

    // If we don't already have defaults for this node set, use our own.
    if (!$curdefaults[$node->type]) {
      $curdefaults[$node->type] = array(
        'enabled' => false,
        'expiration_type' => 'none',
        'expire_timefrom' => '+3 months',
        'expire' => '+3 months',
        'isroot' => false,
      );
    }
    $form[$node->type] = array(
      '#type' => 'fieldset',
      '#title' => $node->name,
      '#tree' => TRUE,
      '#collapsible' => TRUE,
      '#collapsed' => $curdefaults[$node->type]['enabled'] == false,
      '#description' => $node->module == 'book' ? t('These defaults will only be used when no inheritance is available.') : '',
    );
    $form[$node->type]['enabled'] = array(
      '#type' => 'checkbox',
      '#title' => t('Enable expiration for this node type.'),
      '#default_value' => $curdefaults[$node->type]['enabled'],
    );
    $form[$node->type]['expiration_type'] = array(
      '#title' => t('Expiration Type'),
      '#description' => t('What type of node expiration should this node have?'),
      '#type' => 'select',
      '#options' => array(
        'none' => t('Doesn\'t Expire'),
        'date' => t('Expire on Date'),
        'onupdate' => t('Expire After Last Update'),
      ),
      '#default_value' => $curdefaults[$node->type]['expiration_type'],
    );
    $form[$node->type]['expire_timefrom'] = array(
      '#title' => t('Expiration Time'),
      '#description' => t('Time after last update to consider the node expired.'),
      '#type' => 'select',
      '#options' => $period,
      '#default_value' => $curdefaults[$node->type]['expire_timefrom'],
    );
    $form[$node->type]['expire'] = array(
      '#title' => t('Expiration Date'),
      '#description' => t('Time/date to consider the node expired. Format: %time or PHP <a href="http://www.php.net/strtotime" ' . 'target="_blank">strtotime format</a>. Note that if the default date entered is in the past at the node post time, and ' . 'the end-user doesn\'t have access to edit it, the expiration settings for that node will be removed.', array(
        '%time' => format_date(time(), 'large'),
      )),
      '#type' => 'textfield',
      '#default_value' => $curdefaults[$node->type]['expire'],
      '#attributes' => array(
        'class' => 'jscalendar',
      ),
    );

    // As book page is the only node type that allows inheritance, only show it there.
    if ($node->module == 'book' && variable_get('node-expire-book-props', 1) == 1) {
      $form[$node->type]['isroot'] = array(
        '#title' => t('Block Inheritance'),
        '#description' => t('Whether or not to block inheritance of the above settings from parent nodes.'),
        '#type' => 'checkbox',
        '#default_value' => $curdefaults[$node->type]['isroot'],
      );
    }
  }
  return system_settings_form($form);
}

/**
 * Implementation of hook_form_validate()
 **/
function node_expire_default_settings_form_validate($form_id, $form_values) {

  // Only validate the form if we're saving changes. We don't care about values if we're just resetting them anyway.
  if ($form_values['op'] == t('Save configuration')) {

    // The only field we have to check is the expiration date
    foreach ($form_values as $key => $val) {
      if (is_array($val) && isset($val['expire']) && $val['expiration_type'] != 'onupdate') {
        if (($thetime = strtotime($val['expire'])) === false) {
          form_set_error($key . '][expire', t('The entered expiration date is invalid.'));
        }
        else {
          if ($thetime <= time()) {
            form_set_error($key . '][expire', t('The entered expiration date occurrs in the past.'));
          }
        }
      }
    }
  }
}

/**
 * Implementation of hook_form_submit()
 **/
function node_expire_default_settings_form_submit($form_id, $form_values) {

  // Do we want to reset to the defaults?
  if ($form_values['op'] == t('Reset to defaults')) {
    variable_del('node-expire-node-visibility');
    drupal_set_message(t('Settings reset back to defaults.'));
  }
  else {

    // Generate the settings as we need them for our module
    $node_visibility = array();
    foreach ($form_values as $key => $val) {
      if (is_array($val) && isset($val['enabled']) && $val['enabled'] == true) {
        $node_visibility[$key] = array(
          'enabled' => true,
          'expiration_type' => $val['expiration_type'],
          'expire_timefrom' => $val['expire_timefrom'],
          'expire' => $val['expiration_type'] == 'onupdate' ? '' : $val['expire'],
          'isroot' => isset($val['isroot']) ? $val['isroot'] : false,
        );
      }
    }
    variable_set('node-expire-node-visibility', $node_visibility);

    // Delete expirations from database if they no longer pertain to this module
    if (count($node_visibility) == 0) {
      db_query("DELETE FROM {node_expire}");
    }
    else {
      $allowed = array();
      foreach (array_keys($node_visibility) as $val) {
        $allowed[] = "'" . $val . "'";
      }
      $query = db_query("SELECT a.nid FROM {node_expire} a LEFT JOIN {node} b on a.nid = b.nid WHERE b.type NOT IN (" . implode(', ', $allowed) . ")");
      $delete = array();
      while ($row = db_fetch_object($query)) {
        $delete[] = $row->nid;
      }
      if (count($delete) > 0) {

        // Using the normal db_query method, drupal escapes the quotes necessary for the query.
        // This data should be safe anyways since it's pulling from the validated node types.
        db_query("DELETE FROM {node_expire} WHERE nid IN(" . implode(', ', $delete) . ")");
      }
    }
    drupal_set_message(t('Saved new settings.'));
  }
}

/**
 * List all currently expired nodes
 **/
function node_expire_outdated_nodes() {

  // Prepare for the content
  $header = array(
    array(
      'data' => t('Title'),
      'field' => 'b.title',
    ),
    array(
      'data' => t('Last Updated'),
      'field' => 'b.changed',
      'width' => '150px',
    ),
    array(
      'data' => t('Expiration Date'),
      'field' => 'a.expire',
      'sort' => 'asc',
      'width' => '150px',
    ),
    array(
      'data' => t('Owner'),
      'field' => 'c.name',
    ),
    array(
      'data' => t('Operations'),
      'colspan' => '2',
    ),
  );

  // Figure out what documents are old
  $query = db_query("SELECT a.nid, b.title, b.changed, a.expire, IF(c.name = '' OR c.name IS NULL, '%s', c.name) as name FROM {node_expire} a LEFT JOIN {node} b ON " . "a.nid = b.nid LEFT JOIN {users} c ON b.uid = c.uid WHERE a.expire <= NOW() AND a.expiremode != 'none' AND b.status = 1" . tablesort_sql($header), variable_get('anonymous', 'Anonymous'));
  $rows = array();
  while ($row = db_fetch_object($query)) {
    $rows[] = array(
      $row->title,
      format_date($row->changed, 'small'),
      $row->expire == '0000-00-00 00:00:00' ? 'Never' : format_date(strtotime($row->expire), 'small'),
      $row->name,
      array(
        'data' => l(t('view'), 'node/' . $row->nid),
        'align' => 'center',
        'width' => '50px',
      ),
      array(
        'data' => l(t('edit'), 'node/' . $row->nid . '/edit'),
        'align' => 'center',
        'width' => '50px',
      ),
    );
  }

  // No results? Everything must be current.
  if (count($rows) == 0) {
    $rows[] = array(
      array(
        'data' => t('No nodes are expired!'),
        'colspan' => '5',
        'align' => 'center',
      ),
    );
  }
  return theme('table', $header, $rows);
}

/**
 * Add expiration options to the node entry forms
 */
function node_expire_form_alter($form_id, &$form) {
  $node = $form['#node'];
  if (isset($form['type']) && $form['type']['#value'] . '_node_form' == $form_id && !isset($node->expire_disabled)) {
    $form['expiration'] = array(
      '#title' => t('Expiration'),
      '#type' => 'fieldset',
      '#tree' => FALSE,
      '#collapsible' => TRUE,
      '#collapsed' => $node->expiration_type == 'none',
    );
    $form['expiration']['expiration_type'] = array(
      '#title' => t('Expiration Type'),
      '#description' => t('What type of node expiration should this node have?'),
      '#type' => 'select',
      '#options' => array(
        'none' => t('Doesn\'t Expire'),
        'date' => t('Expire on Date'),
        'onupdate' => t('Expire After Last Update'),
      ),
      '#default_value' => $node->expiration_type,
    );
    $period = array(
      '+1 day' => t('1 Day'),
      '+2 days' => t('2 Days'),
      '+3 days' => t('3 Days'),
      '+4 days' => t('4 Days'),
      '+5 days' => t('5 Days'),
      '+6 days' => t('6 Days'),
      '+1 week' => t('1 Week'),
      '+2 weeks' => t('2 Weeks'),
      '+3 weeks' => t('3 Weeks'),
      '+1 month' => t('1 Month'),
      '+2 months' => t('2 Months'),
      '+3 months' => t('3 Months'),
      '+4 months' => t('4 Months'),
      '+5 months' => t('5 Months'),
      '+6 months' => t('6 Months'),
      '+7 months' => t('7 Months'),
      '+8 months' => t('8 Months'),
      '+9 months' => t('9 Months'),
      '+10 months' => t('10 Months'),
      '+11 months' => t('11 Months'),
      '+1 Year' => t('1 Year'),
    );
    $form['expiration']['expire_timefrom'] = array(
      '#title' => t('Expiration Time'),
      '#description' => t('Time after last update to consider the node expired.'),
      '#type' => 'select',
      '#options' => $period,
      '#default_value' => $node->expire_timefrom,
    );
    $form['expiration']['expire'] = array(
      '#title' => t('Expiration Date'),
      '#description' => t('Time date to consider the node expired. Format: %time.', array(
        '%time' => $node->expire,
      )),
      '#type' => 'textfield',
      '#maxlength' => 25,
      '#default_value' => $node->expire,
      '#attributes' => array(
        'class' => 'jscalendar',
      ),
    );

    // As book pages is the only node type that allows inheritance, only show it there.
    if ($form['type']['#value'] == 'book' && variable_get('node-expire-book-props', 1) == 1) {
      $form['expiration']['isroot'] = array(
        '#title' => t('Block Inheritance'),
        '#description' => t('Whether or not to block inheritance of the above settings from parent nodes.'),
        '#type' => 'checkbox',
        '#default_value' => $node->isroot,
      );
    }
  }
}

/**
 * Prepare and parse the data from our node entry forms.
 **/
function node_expire_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  switch ($op) {
    case 'view':
    case 'print':
      $query = db_query('SELECT expire, expiremode FROM {node_expire} WHERE nid = %d', $node->nid);

      // Use the existing expiration data if present.
      if (db_num_rows($query) > 0) {
        $row = db_fetch_object($query);
        $node->expire = $row->expire ? format_date(strtotime($row->expire), 'large') : '';
        $node->expiration_type = $row->expiremode;
      }
      else {
        $node->expire = '';
        $node->expiration_type = 'none';
      }
      break;
    case 'prepare':

      // Is the expiration feature enabled for this node type?
      $curdefaults = variable_get('node-expire-node-visibility', array());
      if (!isset($curdefaults[$node->type]) || !user_access('edit expirations')) {
        $node->expire_disabled = true;
      }
      else {
        $curdefaults = $curdefaults[$node->type];
        $query = db_query('SELECT expire, expiresec, expiremode, isroot FROM {node_expire} WHERE nid = %d', $node->nid);

        // Use the existing expiration data if present.
        if (db_num_rows($query) > 0) {
          $row = db_fetch_object($query);
          $node->expiration_type = $row->expiremode;
          $node->isroot = $row->isroot;
          if ($node->expiration_type == 'none') {
            $node->expire_timefrom = $curdefaults['expire_timefrom'];
            $node->expire = format_date(strtotime($curdefaults['expire_timefrom']), 'custom', 'Y-m-d H:i:s');
          }
          else {
            $node->expire = $row->expire;
            $node->expire_timefrom = $row->expiresec;
          }
        }
        else {

          // If this is a new book page, and inheritance is enabled, let's inherit the data.
          if ($node->type == 'book' && variable_get('node-expire-book-props', 1) && arg(4) > 0) {
            $query = db_query('SELECT expire, expiresec, expiremode FROM {node_expire} WHERE nid = %d', arg(4));

            // If, for whatever reason, no parent data is available, use the defaults
            if (db_num_rows($query) == 0) {
              $node->expire = format_date(strtotime($curdefaults['expire_timefrom']), 'custom', 'Y-m-d H:i:s');
              $node->expire_timefrom = $curdefaults['expire_timefrom'];
              $node->isroot = $curdefaults['isroot'];
              $node->expiration_type = $curdefaults['expiration_type'];
              if ($curdefaults['expiration_type'] == 'onupdate') {
                $node->expire = format_date(strtotime($curdefaults['expire_timefrom']), 'custom', 'Y-m-d H:i:s');
              }
              else {
                $node->expire = format_date(strtotime($curdefaults['expire']), 'custom', 'Y-m-d H:i:s');
              }
            }
            else {
              $row = db_fetch_object($query);
              $node->expire = $row->expire;
              $node->expire_timefrom = $row->expiresec;
              $node->expiration_type = $row->expiremode;
              $node->isroot = 0;
            }
          }
          else {
            $node->expire = format_date(strtotime($curdefaults['expire_timefrom']), 'custom', 'Y-m-d H:i:s');
            $node->expire_timefrom = $curdefaults['expire_timefrom'];
            $node->isroot = $curdefaults['isroot'];
            $node->expiration_type = $curdefaults['expiration_type'];
            if ($curdefaults['expiration_type'] == 'onupdate') {
              $node->expire = format_date(strtotime($curdefaults['expire_timefrom']), 'custom', 'Y-m-d H:i:s');
            }
            else {
              $node->expire = format_date(strtotime($curdefaults['expire']), 'custom', 'Y-m-d H:i:s');
            }
          }
        }
      }
      break;
    case 'validate':

      // The only restriction we have is that the node can't expire in the past.
      if ($node->expiration_type == 'date') {
        if (strtotime($node->expire) <= 0) {
          form_set_error('expire_date', t('You have to specify a valid date.'));
        }
        else {
          if (strtotime($node->expire) <= time()) {
            form_set_error('expire_date', t('You can\'t expire a node in the past!'));
          }
        }
      }
      break;
    case 'insert':
    case 'update':

      // We only want to deal with the database if the expiration feature is available for this node type
      $curdefaults = variable_get('node-expire-node-visibility', array());
      if (isset($curdefaults[$node->type])) {

        // Do we need to update the database?
        $update = 1;

        // We only care about the defaults for this node type
        $curdefaults = $curdefaults[$node->type];

        // Does this user have access to change any expiration settings?
        if (!user_access('edit expirations')) {

          // Does this node already have data for us to work with?
          $query = db_query('SELECT expire, expiresec, expiremode, isroot FROM {node_expire} WHERE nid = %d', $node->nid);
          if (db_num_rows($query) > 0) {
            $row = db_fetch_object($query);

            // Let's keep our current settings
            $node->expire = $row->expire;
            $node->expire_timefrom = $row->expiresec;
            $node->expiration_type = $row->expiremode;
            $node->isroot = $row->expiresec ? 1 : 0;

            // If this node is an "on update" expiration, update the expiration time.
            if ($node->expiration_type == 'onupdate') {
              $node->expire = strtotime($node->expire_timefrom);
            }
            else {
              $update = 0;
            }
          }
          else {
            if ($node->type == 'book' && variable_get('node-expire-book-props', 1) && $node->parent > 0) {
              $query = db_query('SELECT expire, expiresec, expiremode FROM {node_expire} WHERE nid = %d', $node->parent);
              if (db_num_rows($query) > 0) {
                $row = db_fetch_object($query);
                $node->expire = $row->expire;
                $node->expire_timefrom = $row->expiresec;
                $node->expiration_type = $row->expiremode;
                $node->isroot = 0;

                // If the parent node is set to base expiration off last update time, let's mark this node accordingly.
                if ($node->expiration_type == 'onupdate') {
                  $node->expire = strtotime($node->expire_timefrom);
                }
              }
              else {
                $node->expire = strtotime($curdefaults['expire']);
                $node->expire_timefrom = $curdefaults['expire_timefrom'];
                $node->isroot = $curdefaults['isroot'];
                $node->expiration_type = $curdefaults['expiration_type'];
                if ($node->expiration_type == 'onupdate') {
                  $node->expire = strtotime($node->expire_timefrom);
                }

                /**
                 * The user can't change these settings, and it would be set to expire already,
                 * so, let's just not expire the node at all.
                 */
                if ($node->expire <= time()) {
                  $node->expiration_type = 'none';
                }
              }
            }
            else {
              $node->expire = strtotime($curdefaults['expire']);
              $node->expire_timefrom = $curdefaults['expire_timefrom'];
              $node->isroot = $curdefaults['isroot'];
              $node->expiration_type = $curdefaults['expiration_type'];
              if ($node->expiration_type == 'onupdate') {
                $node->expire = strtotime($node->expire_timefrom);
              }

              /**
               * The user can't change these settings, and it would be set to expire already,
               * so, let's just not expire the node at all.
               */
              if ($node->expire <= time()) {
                $node->expiration_type = 'none';
              }
            }
          }

          // Get the information SQL ready - format_date screws up the time zones so we don't want it.
          $node->expire = $node->expire ? date('Y-m-d H:i:s', $node->expire) : '1980-01-01 00:00:00';
        }
        else {

          // If this is a book page, and it's not marked as a root book, override the user's input with the parent's data.
          if ($node->type == 'book' && variable_get('node-expire-book-props', 1) && $node->parent > 0 && $node->isroot == 0) {
            $query = db_query('SELECT expire, expiresec, expiremode FROM {node_expire} WHERE nid = %d', $node->parent);
            if (db_num_rows($query) > 0) {
              $row = db_fetch_object($query);
              $node->expire = $row->expire;
              $node->expire_timefrom = $row->expiresec;
              $node->expiration_type = $row->expiremode;
              $node->isroot = 0;

              // If the parent node is set to base expiration off last update time, let's mark this node accordingly.
              if ($node->expiration_type == 'onupdate') {
                $node->expire = strtotime($node->expire_timefrom);
              }
            }
            else {
              if ($node->expiration_type == 'date') {
                $node->expire = strtotime($node->expire);
                $node->expire_timefrom = '';
              }
              else {
                if ($node->expiration_type == 'onupdate') {
                  $node->expire = strtotime($node->expire_timefrom);
                }
                else {
                  $node->expiration_type = 'none';
                  $node->expire = 0;
                  $node->expire_timefrom = 0;
                }
              }
            }
          }
          else {
            if ($node->expiration_type == 'date') {
              $node->expire = strtotime($node->expire);
              $node->expire_timefrom = '';
            }
            else {
              if ($node->expiration_type == 'onupdate') {
                $node->expire = strtotime($node->expire_timefrom);
              }
              else {
                $node->expiration_type = 'none';
                $node->expire = 0;
                $node->expire_timefrom = 0;
              }
            }
          }

          // Get the information SQL ready - format_date screws up the time zones so we don't want it.
          $node->expire = $node->expire ? date('Y-m-d H:i:s', $node->expire) : '1980-01-01 00:00:00';

          /**
           * Propagate the settings to the child nodes, only if enabled. Notice this is in the section for people with access to edit these settings.
           * It's a waste of resources to perform the recursion task as nothing will be changed.
           **/
          if ($node->type == 'book' && variable_get('node-expire-book-props', 1)) {
            _node_expire_propagate_new($node->nid, $node->expire, $node);
          }
        }

        // To keep track of inheritances and other such things, every node records its expiration settings, not just ones set to expire.
        if ($update) {
          db_query('DELETE FROM {node_expire} WHERE nid = %d', $node->nid);
          db_query("INSERT INTO {node_expire} (nid, expire, expiresec, expiremode, isroot) VALUES (%d, '%s', '%s', '%s', %d)", $node->nid, $node->expire, $node->expire_timefrom, $node->expiration_type, $node->isroot);
        }
      }
      break;
    case 'delete':
      db_query('DELETE FROM {node_expire} WHERE nid = %d', $node->nid);
      break;
  }
}

/**
 * Recursion for inheritance.
 **/
function _node_expire_propagate_new($nid, $changed, $node) {

  // Get a list of all the children
  $query = db_query('SELECT a.nid, c.changed FROM {book} a LEFT JOIN {node_expire} b ON a.nid = b.nid LEFT JOIN {node} c ON a.nid = c.nid WHERE a.parent = %d AND ' . 'COALESCE(b.isroot, 0) = 0', $nid);
  while ($row = db_fetch_object($query)) {
    _node_expire_propagate_new($row->nid, $row->changed, $node);
  }

  // Update the expiration time according to last update of the node itself
  if ($node->expiration_type == 'onupdate') {
    $changed = date('Y-m-d H:i:s', strtotime($node->expire_timefrom, $changed));
  }
  else {
    $changed = $node->expire;
  }

  // To keep track of inheritances and other such things, every node records its expiration settings, not just ones set to expire.
  db_query('DELETE FROM {node_expire} WHERE nid = %d', $nid);
  db_query("INSERT INTO {node_expire} (nid, expire, expiresec, expiremode, isroot) VALUES (%d, '%s', '%s', '%s', 0)", $nid, $changed, $node->expire_timefrom, $node->expiration_type);
  return true;
}

Functions

Namesort descending Description
node_expire_cron Implementation of hook_cron().
node_expire_default_settings_form Configuration form for default expirations for node_expire
node_expire_default_settings_form_submit Implementation of hook_form_submit()
node_expire_default_settings_form_validate Implementation of hook_form_validate()
node_expire_form_alter Add expiration options to the node entry forms
node_expire_menu Implementation of hook_menu().
node_expire_nodeapi Prepare and parse the data from our node entry forms.
node_expire_outdated_nodes List all currently expired nodes
node_expire_perm Implementation of hook_perm().
node_expire_settings_form Configuration form for node_expire
node_expire_settings_form_submit Implementation of hook_form_submit()
node_expire_settings_form_validate Implementation of hook_form_validate()
_node_expire_propagate_new Recursion for inheritance.