diff --git a/phpBB/common.php b/phpBB/common.php
index 0ac7cbbd86..68be033578 100644
--- a/phpBB/common.php
+++ b/phpBB/common.php
@@ -239,3 +239,8 @@ foreach ($cache->obtain_hooks() as $hook)
{
@include($phpbb_root_path . 'includes/hooks/' . $hook . '.' . $phpEx);
}
+
+if (!$config['use_system_cron'])
+{
+ $cron = new phpbb_cron_manager($phpbb_root_path . 'includes/cron/task', $phpEx, $cache->get_driver());
+}
diff --git a/phpBB/cron.php b/phpBB/cron.php
index 4462f52e93..6de493f0bf 100644
--- a/phpBB/cron.php
+++ b/phpBB/cron.php
@@ -20,266 +20,102 @@ include($phpbb_root_path . 'common.' . $phpEx);
$user->session_begin(false);
$auth->acl($user->data);
-$cron_type = request_var('cron_type', '');
-$use_shutdown_function = (@function_exists('register_shutdown_function')) ? true : false;
-
-// Output transparent gif
-header('Cache-Control: no-cache');
-header('Content-type: image/gif');
-header('Content-length: 43');
-
-echo base64_decode('R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==');
-
-// test without flush ;)
-// flush();
-
-//
-if (!isset($config['cron_lock']))
+function output_image()
{
- set_config('cron_lock', '0', true);
+ // Output transparent gif
+ header('Cache-Control: no-cache');
+ header('Content-type: image/gif');
+ header('Content-length: 43');
+
+ echo base64_decode('R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==');
+
+ // Flush here to prevent browser from showing the page as loading while
+ // running cron.
+ flush();
}
-// make sure cron doesn't run multiple times in parallel
-if ($config['cron_lock'])
+function do_cron($cron_lock, $run_tasks)
{
- // if the other process is running more than an hour already we have to assume it
- // aborted without cleaning the lock
- $time = explode(' ', $config['cron_lock']);
- $time = $time[0];
+ global $config;
- if ($time + 3600 >= time())
+ foreach ($run_tasks as $task)
{
- exit;
+ if (defined('DEBUG_EXTRA') && $config['use_system_cron'])
+ {
+ echo "[phpBB cron] Running task '{$task->get_name()}'\n";
+ }
+
+ $task->run();
}
-}
-define('CRON_ID', time() . ' ' . unique_id());
-
-$sql = 'UPDATE ' . CONFIG_TABLE . "
- SET config_value = '" . $db->sql_escape(CRON_ID) . "'
- WHERE config_name = 'cron_lock' AND config_value = '" . $db->sql_escape($config['cron_lock']) . "'";
-$db->sql_query($sql);
-
-// another cron process altered the table between script start and UPDATE query so exit
-if ($db->sql_affectedrows() != 1)
-{
- exit;
-}
-
-/**
-* Run cron-like action
-* Real cron-based layer will be introduced in 3.2
-*/
-switch ($cron_type)
-{
- case 'queue':
-
- if (time() - $config['queue_interval'] <= $config['last_queue_run'] || !file_exists($phpbb_root_path . 'cache/queue.' . $phpEx))
- {
- break;
- }
-
- // A user reported using the mail() function while using shutdown does not work. We do not want to risk that.
- if ($use_shutdown_function && !$config['smtp_delivery'])
- {
- $use_shutdown_function = false;
- }
-
- include_once($phpbb_root_path . 'includes/functions_messenger.' . $phpEx);
- $queue = new queue();
-
- if ($use_shutdown_function)
- {
- register_shutdown_function(array(&$queue, 'process'));
- }
- else
- {
- $queue->process();
- }
-
- break;
-
- case 'tidy_cache':
-
- if (time() - $config['cache_gc'] <= $config['cache_last_gc'] || !method_exists($cache, 'tidy'))
- {
- break;
- }
-
- if ($use_shutdown_function)
- {
- register_shutdown_function(array(&$cache, 'tidy'));
- }
- else
- {
- $cache->tidy();
- }
-
- break;
-
- case 'tidy_search':
-
- // Select the search method
- $search_type = basename($config['search_type']);
-
- if (time() - $config['search_gc'] <= $config['search_last_gc'] || !file_exists($phpbb_root_path . 'includes/search/' . $search_type . '.' . $phpEx))
- {
- break;
- }
-
- include_once("{$phpbb_root_path}includes/search/$search_type.$phpEx");
-
- // We do some additional checks in the module to ensure it can actually be utilised
- $error = false;
- $search = new $search_type($error);
-
- if ($error)
- {
- break;
- }
-
- if ($use_shutdown_function)
- {
- register_shutdown_function(array(&$search, 'tidy'));
- }
- else
- {
- $search->tidy();
- }
-
- break;
-
- case 'tidy_warnings':
-
- if (time() - $config['warnings_gc'] <= $config['warnings_last_gc'])
- {
- break;
- }
-
- include_once($phpbb_root_path . 'includes/functions_admin.' . $phpEx);
-
- if ($use_shutdown_function)
- {
- register_shutdown_function('tidy_warnings');
- }
- else
- {
- tidy_warnings();
- }
-
- break;
-
- case 'tidy_database':
-
- if (time() - $config['database_gc'] <= $config['database_last_gc'])
- {
- break;
- }
-
- include_once($phpbb_root_path . 'includes/functions_admin.' . $phpEx);
-
- if ($use_shutdown_function)
- {
- register_shutdown_function('tidy_database');
- }
- else
- {
- tidy_database();
- }
-
- break;
-
- case 'tidy_sessions':
-
- if (time() - $config['session_gc'] <= $config['session_last_gc'])
- {
- break;
- }
-
- if ($use_shutdown_function)
- {
- register_shutdown_function(array(&$user, 'session_gc'));
- }
- else
- {
- $user->session_gc();
- }
-
- break;
-
- case 'prune_forum':
-
- $forum_id = request_var('f', 0);
-
- $sql = 'SELECT forum_id, prune_next, enable_prune, prune_days, prune_viewed, forum_flags, prune_freq
- FROM ' . FORUMS_TABLE . "
- WHERE forum_id = $forum_id";
- $result = $db->sql_query($sql);
- $row = $db->sql_fetchrow($result);
- $db->sql_freeresult($result);
-
- if (!$row)
- {
- break;
- }
-
- // Do the forum Prune thang
- if ($row['prune_next'] < time() && $row['enable_prune'])
- {
- include_once($phpbb_root_path . 'includes/functions_admin.' . $phpEx);
-
- if ($row['prune_days'])
- {
- if ($use_shutdown_function)
- {
- register_shutdown_function('auto_prune', $row['forum_id'], 'posted', $row['forum_flags'], $row['prune_days'], $row['prune_freq']);
- }
- else
- {
- auto_prune($row['forum_id'], 'posted', $row['forum_flags'], $row['prune_days'], $row['prune_freq']);
- }
- }
-
- if ($row['prune_viewed'])
- {
- if ($use_shutdown_function)
- {
- register_shutdown_function('auto_prune', $row['forum_id'], 'viewed', $row['forum_flags'], $row['prune_viewed'], $row['prune_freq']);
- }
- else
- {
- auto_prune($row['forum_id'], 'viewed', $row['forum_flags'], $row['prune_viewed'], $row['prune_freq']);
- }
- }
- }
-
- break;
-}
-
-// Unloading cache and closing db after having done the dirty work.
-if ($use_shutdown_function)
-{
- register_shutdown_function('unlock_cron');
- register_shutdown_function('garbage_collection');
-}
-else
-{
- unlock_cron();
+ // Unloading cache and closing db after having done the dirty work.
+ $cron_lock->release();
garbage_collection();
}
-exit;
+// Thanks to various fatal errors and lack of try/finally, it is quite easy to leave
+// the cron lock locked, especially when working on cron-related code.
+//
+// Attempt to alleviate the problem by doing setup outside of the lock as much as possible.
+//
+// If DEBUG_EXTRA is defined and cron lock cannot be obtained, a message will be printed.
-
-/**
-* Unlock cron script
-*/
-function unlock_cron()
+if ($config['use_system_cron'])
{
- global $db;
+ $use_shutdown_function = false;
- $sql = 'UPDATE ' . CONFIG_TABLE . "
- SET config_value = '0'
- WHERE config_name = 'cron_lock' AND config_value = '" . $db->sql_escape(CRON_ID) . "'";
- $db->sql_query($sql);
+ $cron = new phpbb_cron_manager($phpbb_root_path . 'includes/cron/task', $phpEx, $cache->get_driver());
+}
+else
+{
+ $cron_type = request_var('cron_type', '');
+ $use_shutdown_function = (@function_exists('register_shutdown_function')) ? true : false;
+
+ // Comment this line out for debugging so the page does not return an image.
+ output_image();
+}
+
+$cron_lock = new phpbb_lock_db('cron_lock', $config, $db);
+if ($cron_lock->acquire())
+{
+ if ($config['use_system_cron'])
+ {
+ $run_tasks = $cron->find_all_ready_tasks();
+ }
+ else
+ {
+ // If invalid task is specified, empty $run_tasks is passed to do_cron which then does nothing
+ $run_tasks = array();
+ $task = $cron->find_task($cron_type);
+ if ($task)
+ {
+ if ($task->is_parametrized())
+ {
+ $task->parse_parameters($request);
+ }
+ if ($task->is_ready())
+ {
+ if ($use_shutdown_function && !$task->is_shutdown_function_safe())
+ {
+ $use_shutdown_function = false;
+ }
+ $run_tasks = array($task);
+ }
+ }
+ }
+ if ($use_shutdown_function)
+ {
+ register_shutdown_function('do_cron', $cron_lock, $run_tasks);
+ }
+ else
+ {
+ do_cron($cron_lock, $run_tasks);
+ }
+}
+else
+{
+ if (defined('DEBUG_EXTRA'))
+ {
+ echo "Could not obtain cron lock.\n";
+ }
}
diff --git a/phpBB/includes/acp/acp_board.php b/phpBB/includes/acp/acp_board.php
index 5dd7673a79..d77fbca7c2 100644
--- a/phpBB/includes/acp/acp_board.php
+++ b/phpBB/includes/acp/acp_board.php
@@ -351,6 +351,7 @@ class acp_board
'vars' => array(
'legend1' => 'ACP_SERVER_SETTINGS',
'gzip_compress' => array('lang' => 'ENABLE_GZIP', 'validate' => 'bool', 'type' => 'radio:yes_no', 'explain' => true),
+ 'use_system_cron' => array('lang' => 'USE_SYSTEM_CRON', 'validate' => 'bool', 'type' => 'radio:yes_no', 'explain' => true),
'legend2' => 'PATH_SETTINGS',
'smilies_path' => array('lang' => 'SMILIES_PATH', 'validate' => 'rpath', 'type' => 'text:20:255', 'explain' => true),
diff --git a/phpBB/includes/cron/manager.php b/phpBB/includes/cron/manager.php
new file mode 100644
index 0000000000..21dcb91695
--- /dev/null
+++ b/phpBB/includes/cron/manager.php
@@ -0,0 +1,251 @@
+task_path = $task_path;
+ $this->phpEx = $phpEx;
+ $this->cache = $cache;
+
+ $task_names = $this->find_cron_task_names();
+ $this->load_tasks($task_names);
+ }
+
+ /**
+ * Finds cron task names.
+ *
+ * A cron task file must follow the naming convention:
+ * includes/cron/task/$mod/$name.php.
+ * $mod is core for tasks that are part of phpbb.
+ * Modifications should use their name as $mod.
+ * $name is the name of the cron task.
+ * Cron task is expected to be a class named phpbb_cron_task_${mod}_${name}.
+ *
+ * @return array List of task names
+ */
+ public function find_cron_task_names()
+ {
+ if ($this->cache)
+ {
+ $task_names = $this->cache->get('_cron_tasks');
+
+ if ($task_names !== false)
+ {
+ return $task_names;
+ }
+ }
+
+ $task_names = array();
+ $ext = '.' . $this->phpEx;
+ $ext_length = strlen($ext);
+
+ $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->task_path));
+
+ foreach ($iterator as $fileinfo)
+ {
+ $file = preg_replace('#^' . preg_quote($this->task_path, '#') . '#', '', $fileinfo->getPathname());
+
+ // skip directories and files direclty in the task root path
+ if ($fileinfo->isFile() && strpos($file, '/') !== false)
+ {
+ $task_name = str_replace('/', '_', substr($file, 0, -$ext_length));
+ if (substr($file, -$ext_length) == $ext && $this->is_valid_name($task_name))
+ {
+ $task_names[] = 'phpbb_cron_task_' . $task_name;
+ }
+ }
+ }
+
+ if ($this->cache)
+ {
+ $this->cache->put('_cron_tasks', $task_names, 3600);
+ }
+
+ return $task_names;
+ }
+
+ /**
+ * Checks whether $name is a valid identifier, and
+ * therefore part of valid cron task class name.
+ *
+ * @param string $name Name to check
+ *
+ * @return bool
+ */
+ public function is_valid_name($name)
+ {
+ return (bool) preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name);
+ }
+
+ /**
+ * Loads tasks given by name, wraps them
+ * and puts them into $this->tasks.
+ *
+ * @param array $task_names Array of strings
+ *
+ * @return void
+ */
+ public function load_tasks(array $task_names)
+ {
+ foreach ($task_names as $task_name)
+ {
+ $task = new $task_name();
+ $wrapper = new phpbb_cron_task_wrapper($task);
+ $this->tasks[] = $wrapper;
+ }
+ }
+
+ /**
+ * Finds a task that is ready to run.
+ *
+ * If several tasks are ready, any one of them could be returned.
+ *
+ * If no tasks are ready, null is returned.
+ *
+ * @return phpbb_cron_task_wrapper|null
+ */
+ public function find_one_ready_task()
+ {
+ foreach ($this->tasks as $task)
+ {
+ if ($task->is_ready())
+ {
+ return $task;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Finds all tasks that are ready to run.
+ *
+ * @return array List of tasks which are ready to run (wrapped in phpbb_cron_task_wrapper).
+ */
+ public function find_all_ready_tasks()
+ {
+ $tasks = array();
+ foreach ($this->tasks as $task)
+ {
+ if ($task->is_ready())
+ {
+ $tasks[] = $task;
+ }
+ }
+ return $tasks;
+ }
+
+ /**
+ * Finds a task by name.
+ *
+ * If there is no task with the specified name, null is returned.
+ *
+ * Web runner uses this method to resolve names to tasks.
+ *
+ * @param string $name Name of the task to look up.
+ * @return phpbb_cron_task A task corresponding to the given name, or null.
+ */
+ public function find_task($name)
+ {
+ foreach ($this->tasks as $task)
+ {
+ if ($task->get_name() == $name)
+ {
+ return $task;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Creates an instance of parametrized cron task $name with args $args.
+ * The constructed task is wrapped with cron task wrapper before being returned.
+ *
+ * @param string $name The task name, which is the same as cron task class name.
+ * @param array $args Will be passed to the task class's constructor.
+ *
+ * @return phpbb_cron_task_wrapper|null
+ */
+ public function instantiate_task($name, array $args)
+ {
+ $task = $this->find_task($name);
+ if ($task)
+ {
+ // task here is actually an instance of cron task wrapper
+ $class = $task->get_name();
+ $task = new $class($args);
+ // need to wrap the new task too
+ $task = new phpbb_cron_task_wrapper($task);
+ }
+ return $task;
+ }
+}
diff --git a/phpBB/includes/cron/task/base.php b/phpBB/includes/cron/task/base.php
new file mode 100644
index 0000000000..38c0b844d9
--- /dev/null
+++ b/phpBB/includes/cron/task/base.php
@@ -0,0 +1,73 @@
+sql_query($sql);
+ while ($row = $db->sql_fetchrow($result))
+ {
+ if ($row['prune_days'])
+ {
+ auto_prune($row['forum_id'], 'posted', $row['forum_flags'], $row['prune_days'], $row['prune_freq']);
+ }
+
+ if ($row['prune_viewed'])
+ {
+ auto_prune($row['forum_id'], 'viewed', $row['forum_flags'], $row['prune_viewed'], $row['prune_freq']);
+ }
+ }
+ $db->sql_freeresult($result);
+ }
+
+ /**
+ * Returns whether this cron task can run, given current board configuration.
+ *
+ * This cron task will only run when system cron is utilised.
+ *
+ * @return bool
+ */
+ public function is_runnable()
+ {
+ global $config;
+ return (bool) $config['use_system_cron'];
+ }
+}
diff --git a/phpBB/includes/cron/task/core/prune_forum.php b/phpBB/includes/cron/task/core/prune_forum.php
new file mode 100644
index 0000000000..55b1c58cd4
--- /dev/null
+++ b/phpBB/includes/cron/task/core/prune_forum.php
@@ -0,0 +1,153 @@
+forum_data = $forum_data;
+ }
+ else
+ {
+ $this->forum_data = null;
+ }
+ }
+
+ /**
+ * Runs this cron task.
+ *
+ * @return void
+ */
+ public function run()
+ {
+ global $phpbb_root_path, $phpEx;
+ if (!function_exists('auto_prune'))
+ {
+ include($phpbb_root_path . 'includes/functions_admin.' . $phpEx);
+ }
+
+ if ($this->forum_data['prune_days'])
+ {
+ auto_prune($this->forum_data['forum_id'], 'posted', $this->forum_data['forum_flags'], $this->forum_data['prune_days'], $this->forum_data['prune_freq']);
+ }
+
+ if ($this->forum_data['prune_viewed'])
+ {
+ auto_prune($this->forum_data['forum_id'], 'viewed', $this->forum_data['forum_flags'], $this->forum_data['prune_viewed'], $this->forum_data['prune_freq']);
+ }
+ }
+
+ /**
+ * Returns whether this cron task can run, given current board configuration.
+ *
+ * This cron task will not run when system cron is utilised, as in
+ * such cases prune_all_forums task would run instead.
+ *
+ * Additionally, this task must be given the forum data, either via
+ * the constructor or parse_parameters method.
+ *
+ * @return bool
+ */
+ public function is_runnable()
+ {
+ global $config;
+ return !$config['use_system_cron'] && $this->forum_data;
+ }
+
+ /**
+ * Returns whether this cron task should run now, because enough time
+ * has passed since it was last run.
+ *
+ * Forum pruning interval is specified in the forum data.
+ *
+ * @return bool
+ */
+ public function should_run()
+ {
+ return $this->forum_data['enable_prune'] && $this->forum_data['prune_next'] < time();
+ }
+
+ /**
+ * Returns parameters of this cron task as an array.
+ * The array has one key, f, whose value is id of the forum to be pruned.
+ *
+ * @return array
+ */
+ public function get_parameters()
+ {
+ return array('f' => $this->forum_data['forum_id']);
+ }
+
+ /**
+ * Parses parameters found in $request, which is an instance of
+ * phpbb_request_interface.
+ *
+ * It is expected to have a key f whose value is id of the forum to be pruned.
+ *
+ * @param phpbb_request_interface $request Request object.
+ *
+ * @return void
+ */
+ public function parse_parameters(phpbb_request_interface $request)
+ {
+ global $db;
+
+ $this->forum_data = null;
+ if ($request->is_set('f'))
+ {
+ $forum_id = $request->variable('f', 0);
+
+ $sql = 'SELECT forum_id, prune_next, enable_prune, prune_days, prune_viewed, forum_flags, prune_freq
+ FROM ' . FORUMS_TABLE . "
+ WHERE forum_id = $forum_id";
+ $result = $db->sql_query($sql);
+ $row = $db->sql_fetchrow($result);
+ $db->sql_freeresult($result);
+
+ if ($row)
+ {
+ $this->forum_data = $row;
+ }
+ }
+ }
+}
diff --git a/phpBB/includes/cron/task/core/queue.php b/phpBB/includes/cron/task/core/queue.php
new file mode 100644
index 0000000000..0e9de05984
--- /dev/null
+++ b/phpBB/includes/cron/task/core/queue.php
@@ -0,0 +1,84 @@
+process();
+ }
+
+ /**
+ * Returns whether this cron task can run, given current board configuration.
+ *
+ * Queue task is only run if the email queue (file) exists.
+ *
+ * @return bool
+ */
+ public function is_runnable()
+ {
+ global $phpbb_root_path, $phpEx;
+ return file_exists($phpbb_root_path . 'cache/queue.' . $phpEx);
+ }
+
+ /**
+ * Returns whether this cron task should run now, because enough time
+ * has passed since it was last run.
+ *
+ * The interval between queue runs is specified in board configuration.
+ *
+ * @return bool
+ */
+ public function should_run()
+ {
+ global $config;
+ return $config['last_queue_run'] < time() - $config['queue_interval_config'];
+ }
+
+ /**
+ * Returns whether this cron task can be run in shutdown function.
+ *
+ * A user reported that using the mail() function during shutdown
+ * function execution does not work. Therefore if email is delivered
+ * via the mail() function (as opposed to SMTP) queue cron task marks
+ * itself shutdown function-unsafe.
+ *
+ * @return bool
+ */
+ public function is_shutdown_function_safe()
+ {
+ global $config;
+ // A user reported using the mail() function while using shutdown does not work. We do not want to risk that.
+ return !$config['smtp_delivery'];
+ }
+}
diff --git a/phpBB/includes/cron/task/core/tidy_cache.php b/phpBB/includes/cron/task/core/tidy_cache.php
new file mode 100644
index 0000000000..793ce746b4
--- /dev/null
+++ b/phpBB/includes/cron/task/core/tidy_cache.php
@@ -0,0 +1,64 @@
+tidy();
+ }
+
+ /**
+ * Returns whether this cron task can run, given current board configuration.
+ *
+ * Tidy cache cron task runs if the cache implementation in use
+ * supports tidying.
+ *
+ * @return bool
+ */
+ public function is_runnable()
+ {
+ global $cache;
+ return method_exists($cache, 'tidy');
+ }
+
+ /**
+ * Returns whether this cron task should run now, because enough time
+ * has passed since it was last run.
+ *
+ * The interval between cache tidying is specified in board
+ * configuration.
+ *
+ * @return bool
+ */
+ public function should_run()
+ {
+ global $config;
+ return $config['cache_last_gc'] < time() - $config['cache_gc'];
+ }
+}
diff --git a/phpBB/includes/cron/task/core/tidy_database.php b/phpBB/includes/cron/task/core/tidy_database.php
new file mode 100644
index 0000000000..fb0e81eaba
--- /dev/null
+++ b/phpBB/includes/cron/task/core/tidy_database.php
@@ -0,0 +1,54 @@
+tidy();
+ }
+ }
+
+ /**
+ * Returns whether this cron task can run, given current board configuration.
+ *
+ * Search cron task is runnable in all normal use. It may not be
+ * runnable if the search backend implementation selected in board
+ * configuration does not exist.
+ *
+ * @return bool
+ */
+ public function is_runnable()
+ {
+ global $phpbb_root_path, $phpEx, $config;
+
+ // Select the search method
+ $search_type = basename($config['search_type']);
+
+ return file_exists($phpbb_root_path . 'includes/search/' . $search_type . '.' . $phpEx);
+ }
+
+ /**
+ * Returns whether this cron task should run now, because enough time
+ * has passed since it was last run.
+ *
+ * The interval between search tidying is specified in board
+ * configuration.
+ *
+ * @return bool
+ */
+ public function should_run()
+ {
+ global $config;
+ return $config['search_last_gc'] < time() - $config['search_gc'];
+ }
+}
diff --git a/phpBB/includes/cron/task/core/tidy_sessions.php b/phpBB/includes/cron/task/core/tidy_sessions.php
new file mode 100644
index 0000000000..81e7e6a147
--- /dev/null
+++ b/phpBB/includes/cron/task/core/tidy_sessions.php
@@ -0,0 +1,50 @@
+session_gc();
+ }
+
+ /**
+ * Returns whether this cron task should run now, because enough time
+ * has passed since it was last run.
+ *
+ * The interval between session tidying is specified in board
+ * configuration.
+ *
+ * @return bool
+ */
+ public function should_run()
+ {
+ global $config;
+ return $config['session_last_gc'] < time() - $config['session_gc'];
+ }
+}
diff --git a/phpBB/includes/cron/task/core/tidy_warnings.php b/phpBB/includes/cron/task/core/tidy_warnings.php
new file mode 100644
index 0000000000..e7d4cc9eea
--- /dev/null
+++ b/phpBB/includes/cron/task/core/tidy_warnings.php
@@ -0,0 +1,69 @@
+task = $task;
+ }
+
+ /**
+ * Returns whether the wrapped task is parametrised.
+ *
+ * Parametrized tasks accept parameters during initialization and must
+ * normally be scheduled with parameters.
+ *
+ * @return bool Whether or not this task is parametrized.
+ */
+ public function is_parametrized()
+ {
+ return $this->task instanceof phpbb_cron_task_parametrized;
+ }
+
+ /**
+ * Returns whether the wrapped task is ready to run.
+ *
+ * A task is ready to run when it is runnable according to current configuration
+ * and enough time has passed since it was last run.
+ *
+ * @return bool Whether the wrapped task is ready to run.
+ */
+ public function is_ready()
+ {
+ return $this->task->is_runnable() && $this->task->should_run();
+ }
+
+ /**
+ * Returns the name of wrapped task. It is the same as the wrapped class's class name.
+ *
+ * @return string Class name of wrapped task.
+ */
+ public function get_name()
+ {
+ return get_class($this->task);
+ }
+
+ /**
+ * Returns a url through which this task may be invoked via web.
+ *
+ * When system cron is not in use, running a cron task is accomplished
+ * by outputting an image with the url returned by this function as
+ * source.
+ *
+ * @return string URL through which this task may be invoked.
+ */
+ public function get_url()
+ {
+ global $phpbb_root_path, $phpEx;
+
+ $name = $this->get_name();
+ if ($this->is_parametrized())
+ {
+ $params = $this->task->get_parameters();
+ $extra = '';
+ foreach ($params as $key => $value)
+ {
+ $extra .= '&' . $key . '=' . urlencode($value);
+ }
+ }
+ else
+ {
+ $extra = '';
+ }
+ $url = append_sid($phpbb_root_path . 'cron.' . $phpEx, 'cron_type=' . $name . $extra);
+ return $url;
+ }
+
+ /**
+ * Forwards all other method calls to the wrapped task implementation.
+ *
+ * @return mixed
+ */
+ public function __call($name, $args)
+ {
+ return call_user_func_array(array($this->task, $name), $args);
+ }
+}
diff --git a/phpBB/includes/functions.php b/phpBB/includes/functions.php
index 056d578e75..418e8dc51d 100644
--- a/phpBB/includes/functions.php
+++ b/phpBB/includes/functions.php
@@ -4595,7 +4595,7 @@ function page_footer($run_cron = true)
// Call cron-type script
$call_cron = false;
- if (!defined('IN_CRON') && $run_cron && !$config['board_disable'])
+ if (!defined('IN_CRON') && !$config['use_system_cron'] && $run_cron && !$config['board_disable'])
{
$call_cron = true;
$time_now = (!empty($user->time_now) && is_int($user->time_now)) ? $user->time_now : time();
@@ -4616,40 +4616,13 @@ function page_footer($run_cron = true)
// Call cron job?
if ($call_cron)
{
- $cron_type = '';
+ global $cron;
+ $task = $cron->find_one_ready_task();
- if ($time_now - $config['queue_interval'] > $config['last_queue_run'] && !defined('IN_ADMIN') && file_exists($phpbb_root_path . 'cache/queue.' . $phpEx))
+ if ($task)
{
- // Process email queue
- $cron_type = 'queue';
- }
- else if (method_exists($cache, 'tidy') && $time_now - $config['cache_gc'] > $config['cache_last_gc'])
- {
- // Tidy the cache
- $cron_type = 'tidy_cache';
- }
- else if ($config['warnings_expire_days'] && ($time_now - $config['warnings_gc'] > $config['warnings_last_gc']))
- {
- $cron_type = 'tidy_warnings';
- }
- else if ($time_now - $config['database_gc'] > $config['database_last_gc'])
- {
- // Tidy the database
- $cron_type = 'tidy_database';
- }
- else if ($time_now - $config['search_gc'] > $config['search_last_gc'])
- {
- // Tidy the search
- $cron_type = 'tidy_search';
- }
- else if ($time_now - $config['session_gc'] > $config['session_last_gc'])
- {
- $cron_type = 'tidy_sessions';
- }
-
- if ($cron_type)
- {
- $template->assign_var('RUN_CRON_TASK', '
');
+ $url = $task->get_url();
+ $template->assign_var('RUN_CRON_TASK', '
');
}
}
diff --git a/phpBB/includes/lock/db.php b/phpBB/includes/lock/db.php
new file mode 100644
index 0000000000..20dbb63e0c
--- /dev/null
+++ b/phpBB/includes/lock/db.php
@@ -0,0 +1,138 @@
+config_name = $config_name;
+ $this->config = $config;
+ $this->db = $db;
+ }
+
+ /**
+ * Tries to acquire the lock by updating
+ * the configuration variable in the database.
+ *
+ * As a lock may only be held by one process at a time, lock
+ * acquisition may fail if another process is holding the lock
+ * or if another process obtained the lock but never released it.
+ * Locks are forcibly released after a timeout of 1 hour.
+ *
+ * @return bool true if lock was acquired
+ * false otherwise
+ */
+ public function acquire()
+ {
+ if ($this->locked)
+ {
+ return false;
+ }
+
+ if (!isset($this->config[$this->config_name]))
+ {
+ $this->config->set($this->config_name, '0', false);
+ }
+ $lock_value = $this->config[$this->config_name];
+
+ // make sure lock cannot be acquired by multiple processes
+ if ($lock_value)
+ {
+ // if the other process is running more than an hour already we have to assume it
+ // aborted without cleaning the lock
+ $time = explode(' ', $lock_value);
+ $time = $time[0];
+
+ if ($time + 3600 >= time())
+ {
+ return false;
+ }
+ }
+
+ $this->unique_id = time() . ' ' . unique_id();
+
+ // try to update the config value, if it was already modified by another
+ // process we failed to acquire the lock.
+ $this->locked = $this->config->set_atomic($this->config_name, $lock_value, $this->unique_id, false);
+
+ return $this->locked;
+ }
+
+ /**
+ * Releases the lock.
+ *
+ * The lock must have been previously obtained, that is, acquire() call
+ * was issued and returned true.
+ *
+ * Note: Attempting to release a lock that is already released,
+ * that is, calling release() multiple times, is harmless.
+ *
+ * @return void
+ */
+ public function release()
+ {
+ if ($this->locked)
+ {
+ $this->config->set_atomic($this->config_name, $this->unique_id, '0', false);
+ $this->locked = false;
+ }
+ }
+}
diff --git a/phpBB/install/database_update.php b/phpBB/install/database_update.php
index 06b37bfcca..c47e9b790a 100644
--- a/phpBB/install/database_update.php
+++ b/phpBB/install/database_update.php
@@ -1863,6 +1863,10 @@ function change_database_data(&$no_updates, $version)
// No changes from 3.0.8-RC1 to 3.0.8
case '3.0.8-RC1':
break;
+
+ case '3.0.9-dev':
+ set_config('use_system_cron', 0);
+ break;
}
}
diff --git a/phpBB/install/schemas/schema_data.sql b/phpBB/install/schemas/schema_data.sql
index 355af802ef..32e411123a 100644
--- a/phpBB/install/schemas/schema_data.sql
+++ b/phpBB/install/schemas/schema_data.sql
@@ -242,6 +242,7 @@ INSERT INTO phpbb_config (config_name, config_value) VALUES ('topics_per_page',
INSERT INTO phpbb_config (config_name, config_value) VALUES ('tpl_allow_php', '0');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('upload_icons_path', 'images/upload_icons');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('upload_path', 'files');
+INSERT INTO phpbb_config (config_name, config_value) VALUES ('use_system_cron', '0');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('version', '3.0.9-dev');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('warnings_expire_days', '90');
INSERT INTO phpbb_config (config_name, config_value) VALUES ('warnings_gc', '14400');
diff --git a/phpBB/language/en/acp/board.php b/phpBB/language/en/acp/board.php
index bdd5f0d2f3..fe023958a9 100644
--- a/phpBB/language/en/acp/board.php
+++ b/phpBB/language/en/acp/board.php
@@ -433,6 +433,8 @@ $lang = array_merge($lang, array(
'SMILIES_PATH_EXPLAIN' => 'Path under your phpBB root directory, e.g. images/smilies.',
'UPLOAD_ICONS_PATH' => 'Extension group icons storage path',
'UPLOAD_ICONS_PATH_EXPLAIN' => 'Path under your phpBB root directory, e.g. images/upload_icons.',
+ 'USE_SYSTEM_CRON' => 'Run periodic tasks from system cron',
+ 'USE_SYSTEM_CRON_EXPLAIN' => 'When off, phpBB will arrange for periodic tasks to be run automatically. When on, phpBB will not schedule any periodic tasks by itself; a system administrator must arrange for cron.php
to be invoked by the system cron facility at regular intervals (e.g. every 5 minutes).',
));
// Security Settings
diff --git a/phpBB/viewforum.php b/phpBB/viewforum.php
index 47d71849cb..2672703042 100644
--- a/phpBB/viewforum.php
+++ b/phpBB/viewforum.php
@@ -193,9 +193,14 @@ if ($forum_data['forum_topics_per_page'])
}
// Do the forum Prune thang - cron type job ...
-if ($forum_data['prune_next'] < time() && $forum_data['enable_prune'])
+if (!$config['use_system_cron'])
{
- $template->assign_var('RUN_CRON_TASK', '
');
+ $task = $cron->instantiate_task('cron_task_core_prune_forum', $forum_data);
+ if ($task && $task->is_ready())
+ {
+ $url = $task->get_url();
+ $template->assign_var('RUN_CRON_TASK', '
');
+ }
}
// Forum rules and subscription info
diff --git a/tests/cron/manager_test.php b/tests/cron/manager_test.php
new file mode 100644
index 0000000000..6288a5c641
--- /dev/null
+++ b/tests/cron/manager_test.php
@@ -0,0 +1,83 @@
+manager = new phpbb_cron_manager(__DIR__ . '/task/', 'php');
+ $this->task_name = 'phpbb_cron_task_testmod_dummy_task';
+ }
+
+ public function test_manager_finds_shipped_tasks()
+ {
+ $tasks = $this->manager->find_cron_task_names();
+ $this->assertEquals(2, sizeof($tasks));
+ }
+
+ public function test_manager_finds_shipped_task_by_name()
+ {
+ $task = $this->manager->find_task($this->task_name);
+ $this->assertInstanceOf('phpbb_cron_task_wrapper', $task);
+ $this->assertEquals($this->task_name, $task->get_name());
+ }
+
+ public function test_manager_instantiates_task_by_name()
+ {
+ $task = $this->manager->instantiate_task($this->task_name, array());
+ $this->assertInstanceOf('phpbb_cron_task_wrapper', $task);
+ $this->assertEquals($this->task_name, $task->get_name());
+ }
+
+ public function test_manager_finds_all_ready_tasks()
+ {
+ $tasks = $this->manager->find_all_ready_tasks();
+ $this->assertEquals(2, sizeof($tasks));
+ }
+
+ public function test_manager_finds_one_ready_task()
+ {
+ $task = $this->manager->find_one_ready_task();
+ $this->assertInstanceOf('phpbb_cron_task_wrapper', $task);
+ }
+
+ public function test_manager_finds_all_ready_tasks_cached()
+ {
+ $cache = new phpbb_mock_cache(array('_cron_tasks' => array($this->task_name)));
+ $manager = new phpbb_cron_manager(__DIR__ . '/../../phpBB/', 'php', $cache);
+
+ $tasks = $manager->find_all_ready_tasks();
+ $this->assertEquals(1, sizeof($tasks));
+ }
+
+ public function test_manager_finds_only_ready_tasks()
+ {
+ $manager = new phpbb_cron_manager(__DIR__ . '/task2/', 'php');
+ $tasks = $manager->find_all_ready_tasks();
+ $task_names = $this->tasks_to_names($tasks);
+ $this->assertEquals(array('phpbb_cron_task_testmod_simple_ready'), $task_names);
+ }
+
+ private function tasks_to_names($tasks)
+ {
+ $names = array();
+ foreach ($tasks as $task)
+ {
+ $names[] = get_class($task->task);
+ }
+ return $names;
+ }
+}
diff --git a/tests/cron/task/testmod/dummy_task.php b/tests/cron/task/testmod/dummy_task.php
new file mode 100644
index 0000000000..5941157589
--- /dev/null
+++ b/tests/cron/task/testmod/dummy_task.php
@@ -0,0 +1,23 @@
+createXMLDataSet(dirname(__FILE__).'/fixtures/config.xml');
+ }
+
+ public function setUp()
+ {
+ global $db, $config;
+
+ $db = $this->db = $this->new_dbal();
+ $config = $this->config = new phpbb_config(array('rand_seed' => '', 'rand_seed_last_update' => '0'));
+ set_config(null, null, null, $this->config);
+ $this->lock = new phpbb_lock_db('test_lock', $this->config, $this->db);
+ }
+
+ public function test_new_lock()
+ {
+ $this->assertTrue($this->lock->acquire());
+ $this->assertTrue(isset($this->config['test_lock']), 'Lock was created');
+
+ $lock2 = new phpbb_lock_db('test_lock', $this->config, $this->db);
+ $this->assertFalse($lock2->acquire());
+
+ $this->lock->release();
+ $this->assertEquals('0', $this->config['test_lock'], 'Lock was released');
+ }
+
+ public function test_expire_lock()
+ {
+ $lock = new phpbb_lock_db('foo_lock', $this->config, $this->db);
+ $this->assertTrue($lock->acquire());
+ }
+
+ public function test_double_lock()
+ {
+ $this->assertTrue($this->lock->acquire());
+ $this->assertTrue(isset($this->config['test_lock']), 'Lock was created');
+
+ $value = $this->config['test_lock'];
+
+ $this->assertFalse($this->lock->acquire());
+ $this->assertEquals($value, $this->config['test_lock'], 'Second lock failed');
+
+ $this->lock->release();
+ $this->assertEquals('0', $this->config['test_lock'], 'Lock was released');
+ }
+
+ public function test_double_unlock()
+ {
+ $this->assertTrue($this->lock->acquire());
+ $this->assertFalse(empty($this->config['test_lock']), 'First lock is acquired');
+
+ $this->lock->release();
+ $this->assertEquals('0', $this->config['test_lock'], 'First lock is released');
+
+ $lock2 = new phpbb_lock_db('test_lock', $this->config, $this->db);
+ $this->assertTrue($lock2->acquire());
+ $this->assertFalse(empty($this->config['test_lock']), 'Second lock is acquired');
+
+ $this->lock->release();
+ $this->assertFalse(empty($this->config['test_lock']), 'Double release of first lock is ignored');
+
+ $lock2->release();
+ $this->assertEquals('0', $this->config['test_lock'], 'Second lock is released');
+ }
+}
diff --git a/tests/lock/fixtures/config.xml b/tests/lock/fixtures/config.xml
new file mode 100644
index 0000000000..f36c8b929a
--- /dev/null
+++ b/tests/lock/fixtures/config.xml
@@ -0,0 +1,13 @@
+
+
+
+ config_name
+ config_value
+ is_dynamic
+
+ foo_lock
+ 1 abcd
+ 1
+
+
+