Index: project_issue.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/project_issue.module,v
retrieving revision 1.97
diff -u -p -r1.97 project_issue.module
--- project_issue.module	29 Oct 2008 00:20:09 -0000	1.97
+++ project_issue.module	28 Dec 2008 11:14:30 -0000
@@ -295,37 +295,20 @@ function project_issue_validate_followup
   }
 }
 
-function project_issue_cron() {
-  if (time() - variable_get('project_issue_digest_last', 0) > variable_get('project_issue_digest_interval', 7 * 24 * 60 * 60)) {
-    variable_set('project_issue_digest_last', time());
-    project_mail_digest();
-  }
-
-  if (time() - variable_get('project_issue_reminder_last', 0) > variable_get('project_issue_reminder_interval', 28 * 7 * 24 * 60 * 60)) {
-    variable_set('project_issue_reminder_last', time());
-    project_mail_reminder();
-  }
-
-  // Auto-close fixed issues;
-  project_issue_auto_close();
-}
-
 /**
- * Automatically close issues marked as fixed for a specified number of days
- * and add a comment to each documenting the change.
+ * Implementation of hook_cron().
+ *
+ * There is a variable (no admin UI, just via settings.php) that controls if
+ * the admin has setup a separate cron job on their system to invoke this code
+ * instead of relying on cron.php and hook_cron(). If this variable, called
+ * 'project_issue_separate_cron', is set to TRUE, then there's nothing to do
+ * in here. Otherwise, we include the cron.inc file and invoke that code
+ * ourselves.
  */
-function project_issue_auto_close() {
-  // Set query parameters.
-  $seconds = 24 * 60 * 60 * variable_get('project_issue_auto_close_days', PROJECT_ISSUE_AUTO_CLOSE_DAYS);
-
-  $comment = theme('project_issue_auto_close_message');
-  $result = db_query('SELECT pi.nid FROM {project_issues} pi INNER JOIN {node} n ON n.nid = pi.nid WHERE pi.sid = %d AND n.changed < %d', PROJECT_ISSUE_STATE_FIXED, time() - $seconds);
-  while ($issue = db_fetch_object($result)) {
-    project_issue_add_followup(array(
-      'nid' => $issue->nid,
-      'sid' => PROJECT_ISSUE_STATE_CLOSED,
-      'comment' => $comment,
-    ));
+function project_issue_cron() {
+  if (variable_get('project_issue_separate_cron', FALSE) == FALSE) {
+    require_once drupal_get_path('module', 'project_issue') .'/includes/cron.inc';
+    _project_issue_cron();
   }
 }
 
Index: project_issue.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/project_issue.install,v
retrieving revision 1.51
diff -u -p -r1.51 project_issue.install
--- project_issue.install	29 Oct 2008 00:20:08 -0000	1.51
+++ project_issue.install	28 Dec 2008 11:14:31 -0000
@@ -208,6 +208,7 @@ function project_issue_uninstall() {
     'project_issue_site_help',
     'project_issue_invalid_releases',
     'project_issue_followup_user',
+    'project_issue_separate_cron',
   );
   foreach ($variables as $variable) {
     variable_del($variable);
Index: mail.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project_issue/mail.inc,v
retrieving revision 1.109
diff -u -p -r1.109 mail.inc
--- mail.inc	29 Oct 2008 00:20:08 -0000	1.109
+++ mail.inc	28 Dec 2008 11:14:32 -0000
@@ -570,102 +570,3 @@ function project_mail_format_entry($entr
   return $output;
 }
 
-function project_mail_reminder() {
-  global $base_url;
-
-  if (defined('PROJECT_NOMAIL')) {
-    return;
-  }
-
-  $projects = array();
-  $result = db_query(db_rewrite_sql('SELECT p.nid, n.title FROM {project_issue_projects} p INNER JOIN {node} n ON p.nid = n.nid WHERE p.mail_reminder = 1 AND n.status = 1', 'p'));
-  while ($project = db_fetch_object($result)) {
-    $projects[$project->nid] = $project->title;
-    $pids[] = "p.pid = $project->nid";
-  }
-  if (count($projects)) {
-
-    $sender->name = variable_get('site_name', '');
-    $sender->mail = variable_get('site_mail', '');
-
-    $result = db_query(db_rewrite_sql('SELECT p.nid, n.*, p.*, u.name, u.mail FROM {project_issues} p INNER JOIN {node} n ON p.nid = n.nid INNER JOIN {users} u ON n.uid = u.uid WHERE n.status = 1 AND u.status = 1 AND ('. implode(' OR ', $pids) .") AND u.mail <> '' AND (p.sid = 1 OR p.sid = 2) ORDER BY u.uid, p.pid, p.component, p.sid, n.changed DESC", 'p'));
-
-    while (($node = db_fetch_object($result)) || !empty($body)) {
-      if ($body && $mail && ((!$node) || ($mail != $node->mail))) {
-        $header = array(
-          'Return-Path' => "<$sender->mail;>",
-          'Date' => date('r'),
-          'From' => "$sender->name <$sender->mail>",
-          'X-Mailer' => 'Drupal Project Issues module (http://drupal.org/project/project_issue)',
-          'List-Id' => "$sender->name <project-reminder-". preg_replace('|.+://([a-zA-Z0-9\._-]+).*|', '\1', $base_url) .'>',
-          'List-Archive' => '<'. url('project', NULL, NULL, 1) .'>',
-        );
-
-        drupal_mail('project_issue_reminder', $mail, t('Your submitted bugs for !date', array('!date' => date('F d, Y', time()))), $body, NULL, $header);
-
-        $body = '';
-      }
-      $mail = $node->mail;
-
-      if ($node->nid) {
-        if ($pid != $node->pid || $component != $node->component) {
-          $pid = $node->pid;
-          $component = $node->component;
-          $banner = "$projects[$pid] / $component";
-          $body .= "[ $banner ]". str_repeat('=', 72 - 4 - strlen($banner)) ."\n";
-        }
-        $body .= "$node->title\n";
-        if ($node->assigned && $assigned = user_load(array('uid' => $node->assigned))) {
-          $body .= "  assigned: $assigned->name\n";
-        }
-        $body .= '  state: '. project_issue_state($node->sid) ."\n";
-        $body .= '  age: '. format_interval(time() - $node->created) ."\n";
-        $body .= '  url: '. url("node/$node->nid", NULL, NULL, 1) ."\n";
-        $body .= "\n";
-      }
-    }
-  }
-}
-
-function project_mail_digest() {
-  global $base_url;
-
-  if (defined('PROJECT_NOMAIL')) {
-    return;
-  }
-
-  $body = array();
-  $header = array(
-    'Date' => date('r'),
-    'X-Mailer' => 'Drupal Project Issues module (http://drupal.org/project/project_issue)',
-    'List-Id' => "$project->title <". preg_replace('|.+://([a-zA-Z0-9\._-]+).*|', '\1', $base_url) .'-project-issues-digest>',
-    'List-Archive' => '<'. url('project/issues?priorities=1', NULL, NULL, 1) .'>',
-  );
-
-  $result = db_query(db_rewrite_sql("SELECT n.nid, n.title, p.* FROM {node} n INNER JOIN {project_issue_projects} p ON n.nid = p.nid WHERE n.status = 1 AND p.mail_digest <> '' ORDER BY n.title, p.mail_digest"));
-  while ($project = db_fetch_object($result)) {
-    $category = '';
-    $result2 = db_query(db_rewrite_sql('SELECT p.nid, n.title, n.created, p.sid, p.category, p.component, p.priority, p.assigned FROM {project_issues} p INNER JOIN {node} n ON p.nid = n.nid WHERE n.status = 1 AND p.pid = %d AND p.sid = 1 AND p.priority = 1 ORDER BY p.category, n.created DESC', 'p'), $project->nid);
-    while ($node = db_fetch_object($result2)) {
-      if ($category != $node->category) {
-        $category = $node->category;
-        $banner = "$project->title / ". project_issue_category($node->category);
-        $body[$project->mail_digest] .= "$banner\n". str_repeat('-', $banner) ."\n";
-      }
-      $body[$project->mail_digest] .= "$node->title\n";
-      if ($node->assigned && $assigned = user_load(array('uid' => $node->assigned))) {
-        $body[$project->mail_digest] .= '  assigned: '. $assigned->name ."\n";
-      }
-      $body[$project->mail_digest] .= '  age: '. format_interval(time() - $node->created) ."\n";
-      $body[$project->mail_digest] .= '  url: '. url("node/$node->nid", NULL, NULL, 1) ."\n";
-      $body[$project->mail_digest] .= "\n";
-    }
-  }
-
-  foreach ($body as $mailto => $text) {
-    $header['From'] = $mailto;
-    $header['Return-Path'] = "<$mailto>";
-
-    drupal_mail('project_issue_critical_summary', $mailto, t('Release critical bugs for !date', array('!date' => date('F d, Y', time()))), $text, NULL, $header);
-  }
-}
cvs diff: Diffing includes
Index: includes/cron.inc
===================================================================
RCS file: includes/cron.inc
diff -N includes/cron.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/cron.inc	28 Dec 2008 11:14:32 -0000
@@ -0,0 +1,156 @@
+<?php
+// $Id$
+
+
+/**
+ * @file
+ * Contains the code required during cron runs for periodic functionality.
+ *
+ * This code is either invoked via hook_cron() or project-issue-cron.php 
+ * depending on the value of the 'project_issue_separate_cron' variable.
+ */
+
+/**
+ * Private helper function to run periodic functionality.
+ *
+ * This code is either invoked by hook_cron() or via project-issue-cron.php
+ * depending on the value of the 'project_issue_separate_cron' variable. It is
+ * responsible for auto-closing issues, and sending email digests and
+ * reminders.
+ *
+ * @see project_issue_cron()
+ */
+function _project_issue_cron() {
+  if (time() - variable_get('project_issue_digest_last', 0) > variable_get('project_issue_digest_interval', 7 * 24 * 60 * 60)) {
+    variable_set('project_issue_digest_last', time());
+    project_mail_digest();
+  }
+
+  if (time() - variable_get('project_issue_reminder_last', 0) > variable_get('project_issue_reminder_interval', 28 * 7 * 24 * 60 * 60)) {
+    variable_set('project_issue_reminder_last', time());
+    project_mail_reminder();
+  }
+
+  // Auto-close fixed issues;
+  project_issue_auto_close();
+}
+
+/**
+ * Automatically close issues marked as fixed for a specified number of days
+ * and add a comment to each documenting the change.
+ */
+function project_issue_auto_close() {
+  // Set query parameters.
+  $seconds = 24 * 60 * 60 * variable_get('project_issue_auto_close_days', PROJECT_ISSUE_AUTO_CLOSE_DAYS);
+
+  $comment = theme('project_issue_auto_close_message');
+  $result = db_query('SELECT pi.nid FROM {project_issues} pi INNER JOIN {node} n ON n.nid = pi.nid WHERE pi.sid = %d AND n.changed < %d', PROJECT_ISSUE_STATE_FIXED, time() - $seconds);
+  while ($issue = db_fetch_object($result)) {
+    project_issue_add_followup(array(
+      'nid' => $issue->nid,
+      'sid' => PROJECT_ISSUE_STATE_CLOSED,
+      'comment' => $comment,
+    ));
+  }
+}
+
+function project_mail_reminder() {
+  global $base_url;
+
+  if (defined('PROJECT_NOMAIL')) {
+    return;
+  }
+
+  $projects = array();
+  $result = db_query(db_rewrite_sql('SELECT p.nid, n.title FROM {project_issue_projects} p INNER JOIN {node} n ON p.nid = n.nid WHERE p.mail_reminder = 1 AND n.status = 1', 'p'));
+  while ($project = db_fetch_object($result)) {
+    $projects[$project->nid] = $project->title;
+    $pids[] = "p.pid = $project->nid";
+  }
+  if (count($projects)) {
+
+    $sender->name = variable_get('site_name', '');
+    $sender->mail = variable_get('site_mail', '');
+
+    $result = db_query(db_rewrite_sql('SELECT p.nid, n.*, p.*, u.name, u.mail FROM {project_issues} p INNER JOIN {node} n ON p.nid = n.nid INNER JOIN {users} u ON n.uid = u.uid WHERE n.status = 1 AND u.status = 1 AND ('. implode(' OR ', $pids) .") AND u.mail <> '' AND (p.sid = 1 OR p.sid = 2) ORDER BY u.uid, p.pid, p.component, p.sid, n.changed DESC", 'p'));
+
+    while (($node = db_fetch_object($result)) || !empty($body)) {
+      if ($body && $mail && ((!$node) || ($mail != $node->mail))) {
+        $header = array(
+          'Return-Path' => "<$sender->mail;>",
+          'Date' => date('r'),
+          'From' => "$sender->name <$sender->mail>",
+          'X-Mailer' => 'Drupal Project Issues module (http://drupal.org/project/project_issue)',
+          'List-Id' => "$sender->name <project-reminder-". preg_replace('|.+://([a-zA-Z0-9\._-]+).*|', '\1', $base_url) .'>',
+          'List-Archive' => '<'. url('project', NULL, NULL, 1) .'>',
+        );
+
+        drupal_mail('project_issue_reminder', $mail, t('Your submitted bugs for !date', array('!date' => date('F d, Y', time()))), $body, NULL, $header);
+
+        $body = '';
+      }
+      $mail = $node->mail;
+
+      if ($node->nid) {
+        if ($pid != $node->pid || $component != $node->component) {
+          $pid = $node->pid;
+          $component = $node->component;
+          $banner = "$projects[$pid] / $component";
+          $body .= "[ $banner ]". str_repeat('=', 72 - 4 - strlen($banner)) ."\n";
+        }
+        $body .= "$node->title\n";
+        if ($node->assigned && $assigned = user_load(array('uid' => $node->assigned))) {
+          $body .= "  assigned: $assigned->name\n";
+        }
+        $body .= '  state: '. project_issue_state($node->sid) ."\n";
+        $body .= '  age: '. format_interval(time() - $node->created) ."\n";
+        $body .= '  url: '. url("node/$node->nid", NULL, NULL, 1) ."\n";
+        $body .= "\n";
+      }
+    }
+  }
+}
+
+function project_mail_digest() {
+  global $base_url;
+
+  if (defined('PROJECT_NOMAIL')) {
+    return;
+  }
+
+  $body = array();
+  $header = array(
+    'Date' => date('r'),
+    'X-Mailer' => 'Drupal Project Issues module (http://drupal.org/project/project_issue)',
+    'List-Id' => "$project->title <". preg_replace('|.+://([a-zA-Z0-9\._-]+).*|', '\1', $base_url) .'-project-issues-digest>',
+    'List-Archive' => '<'. url('project/issues?priorities=1', NULL, NULL, 1) .'>',
+  );
+
+  $result = db_query(db_rewrite_sql("SELECT n.nid, n.title, p.* FROM {node} n INNER JOIN {project_issue_projects} p ON n.nid = p.nid WHERE n.status = 1 AND p.mail_digest <> '' ORDER BY n.title, p.mail_digest"));
+  while ($project = db_fetch_object($result)) {
+    $category = '';
+    $result2 = db_query(db_rewrite_sql('SELECT p.nid, n.title, n.created, p.sid, p.category, p.component, p.priority, p.assigned FROM {project_issues} p INNER JOIN {node} n ON p.nid = n.nid WHERE n.status = 1 AND p.pid = %d AND p.sid = 1 AND p.priority = 1 ORDER BY p.category, n.created DESC', 'p'), $project->nid);
+    while ($node = db_fetch_object($result2)) {
+      if ($category != $node->category) {
+        $category = $node->category;
+        $banner = "$project->title / ". project_issue_category($node->category);
+        $body[$project->mail_digest] .= "$banner\n". str_repeat('-', $banner) ."\n";
+      }
+      $body[$project->mail_digest] .= "$node->title\n";
+      if ($node->assigned && $assigned = user_load(array('uid' => $node->assigned))) {
+        $body[$project->mail_digest] .= '  assigned: '. $assigned->name ."\n";
+      }
+      $body[$project->mail_digest] .= '  age: '. format_interval(time() - $node->created) ."\n";
+      $body[$project->mail_digest] .= '  url: '. url("node/$node->nid", NULL, NULL, 1) ."\n";
+      $body[$project->mail_digest] .= "\n";
+    }
+  }
+
+  foreach ($body as $mailto => $text) {
+    $header['From'] = $mailto;
+    $header['Return-Path'] = "<$mailto>";
+
+    drupal_mail('project_issue_critical_summary', $mailto, t('Release critical bugs for !date', array('!date' => date('F d, Y', time()))), $text, NULL, $header);
+  }
+}
+
cvs diff: Diffing scripts
Index: scripts/.htaccess
===================================================================
RCS file: scripts/.htaccess
diff -N scripts/.htaccess
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ scripts/.htaccess	28 Dec 2008 11:14:32 -0000
@@ -0,0 +1,10 @@
+# The scripts in this directory should never be invoked from the web.
+# They are meant to be run via cron as CLI scripts, preferably as a
+# different user than your httpd. Therefore, we prevent them from
+# being accessible via the web at all.
+
+<FilesMatch "\.php$">
+  Order allow,deny
+</FilesMatch>
+
+# $Id: .htaccess,v 1.1 2007/06/05 20:45:42 dww Exp $
Index: scripts/project-issue-cron.php
===================================================================
RCS file: scripts/project-issue-cron.php
diff -N scripts/project-issue-cron.php
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ scripts/project-issue-cron.php	28 Dec 2008 11:14:32 -0000
@@ -0,0 +1,87 @@
+#!/usr/bin/php
+<?php
+// $Id$
+
+
+/**
+ * @file
+ * Script to invoke project_issue periodic code outside of hook_cron().
+ *
+ * On some sites, hook_cron() becomes very busy and you can frequently get
+ * timeouts where all of the cron-tasks can't be completed during the time
+ * limit for web-based PHP requests. To aleviate this problem, you can move
+ * some of the code invoked by hook_cron() into separate CLI scripts which you
+ * invoke directly by adding additional entries to the crontab on your system.
+ * If you do this, you must also set the "project_issue_separate_cron"
+ * variable to TRUE in your site's settings.php file.
+ *
+ * @author Derek Wright (http://drupal.org/user/46549)
+ *
+ */
+
+// ------------------------------------------------------------
+// Required customization
+// ------------------------------------------------------------
+
+// The root of your Drupal installation, so we can properly bootstrap
+// Drupal. This should be the full path to the directory that holds
+// your index.php file, the "includes" subdirectory, etc.
+define('DRUPAL_ROOT', '');
+
+// The name of your site. Required so that when we bootstrap Drupal in
+// this script, we find the right settings.php file in your sites folder.
+define('SITE_NAME', '');
+
+
+// ------------------------------------------------------------
+// Initialization
+// (Real work begins here, nothing else to customize)
+// ------------------------------------------------------------
+
+// Check if all required variables are defined
+$vars = array(
+  'DRUPAL_ROOT' => DRUPAL_ROOT,
+  'SITE_NAME' => SITE_NAME,
+);
+$fatal_err = FALSE;
+foreach ($vars as $name => $val) {
+  if (empty($val)) {
+    print "ERROR: \"$name\" constant not defined, aborting\n";
+    $fatal_err = TRUE;
+  }
+}
+if ($fatal_err) {
+  exit(1);
+}
+
+$script_name = $argv[0];
+
+// Setup variables for Drupal bootstrap
+$_SERVER['HTTP_HOST'] = SITE_NAME;
+$_SERVER['REQUEST_URI'] = '/' . $script_name;
+$_SERVER['SCRIPT_NAME'] = '/' . $script_name;
+$_SERVER['PHP_SELF'] = '/' . $script_name;
+$_SERVER['SCRIPT_FILENAME'] = $_SERVER['PWD'] .'/'. $script_name;
+$_SERVER['PATH_TRANSLATED'] = $_SERVER['SCRIPT_FILENAME'];
+
+if (!chdir(DRUPAL_ROOT)) {
+  print "ERROR: Can't chdir(DRUPAL_ROOT), aborting.\n";
+  exit(1);
+}
+// Make sure our umask is sane for generating directories and files.
+umask(022);
+
+require_once 'includes/bootstrap.inc';
+drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
+
+if (!module_exists('project_issue')) {
+  wd_err(t("ERROR: Project issue module does not exist, aborting.\n"));
+  exit(1);
+}
+
+// Real work begins.
+if (variable_get('project_issue_separate_cron', FALSE) == TRUE) {
+  require_once drupal_get_path('module', 'project_issue') .'/includes/cron.inc';
+  _project_issue_cron();
+}
+
