| <?php | |
| /** | |
| * This is a slightly strange file. It is not designed to ever be run directly from within SMF's | |
| * conventional running, but called externally to facilitate background tasks. It can be called | |
| * either directly or via cron, and in either case will completely ignore anything supplied | |
| * via command line, or $_GET, $_POST, $_COOKIE etc. because those things should never affect the | |
| * running of this script. | |
| * | |
| * Because of the way this runs, etc. we do need some of SMF but not everything to try to keep this | |
| * running a little bit faster. | |
| * | |
| * Simple Machines Forum (SMF) | |
| * | |
| * @package SMF | |
| * @author Simple Machines http://www.simplemachines.org | |
| * @copyright 2015 Simple Machines and individual contributors | |
| * @license http://www.simplemachines.org/about/smf/license.php BSD | |
| * | |
| * @version 2.1 Beta 2 | |
| */ | |
| define('SMF', 'BACKGROUND'); | |
| define('FROM_CLI', empty($_SERVER['REQUEST_METHOD'])); | |
| // This one setting is worth bearing in mind. If you are running this from proper cron, make sure you | |
| // don't run this file any more frequently than indicated here. It might turn ugly if you do. | |
| // But on proper cron you can always increase this value provided you don't go beyond max_limit. | |
| define('MAX_CRON_TIME', 10); | |
| // If a task fails for whatever reason it will still be marked as claimed. This is the threshold | |
| // by which if a task has not completed in this time, the task should become available again. | |
| define('MAX_CLAIM_THRESHOLD', 300); | |
| // We're going to want a few globals... these are all set later. | |
| global $time_start, $maintenance, $msubject, $mmessage, $mbname, $language; | |
| global $boardurl, $boarddir, $sourcedir, $webmaster_email; | |
| global $db_server, $db_name, $db_user, $db_prefix, $db_persist, $db_error_send, $db_last_error; | |
| global $db_connection, $modSettings, $context, $sc, $user_info, $txt; | |
| global $smcFunc, $ssi_db_user, $scripturl, $db_passwd, $cachedir; | |
| define('TIME_START', microtime(true)); | |
| // Just being safe... | |
| foreach (array('db_character_set', 'cachedir') as $variable) | |
| if (isset($GLOBALS[$variable])) | |
| unset($GLOBALS[$variable]); | |
| // Get the forum's settings for database and file paths. | |
| require_once(dirname(__FILE__) . '/Settings.php'); | |
| // Make absolutely sure the cache directory is defined. | |
| if ((empty($cachedir) || !file_exists($cachedir)) && file_exists($boarddir . '/cache')) | |
| $cachedir = $boarddir . '/cache'; | |
| // Don't do john didley if the forum's been shut down competely. | |
| if ($maintenance == 2) | |
| die($mmessage); | |
| // Fix for using the current directory as a path. | |
| if (substr($sourcedir, 0, 1) == '.' && substr($sourcedir, 1, 1) != '.') | |
| $sourcedir = dirname(__FILE__) . substr($sourcedir, 1); | |
| // Have we already turned this off? If so, exist gracefully. | |
| if (file_exists($cachedir . '/cron.lock')) | |
| obExit_cron(); | |
| // Before we go any further, if this is not a CLI request, we need to do some checking. | |
| if (!FROM_CLI) | |
| { | |
| // We will clean up $_GET shortly. But we want to this ASAP. | |
| $ts = isset($_GET['ts']) ? (int) $_GET['ts'] : 0; | |
| if ($ts <= 0 || $ts % 15 != 0 || time() - $ts < 0 || time() - $ts > 20) | |
| obExit_cron(); | |
| } | |
| // Load the most important includes. In general, a background should be loading its own dependencies. | |
| require_once($sourcedir . '/Errors.php'); | |
| require_once($sourcedir . '/Load.php'); | |
| require_once($sourcedir . '/Subs.php'); | |
| // Create a variable to store some SMF specific functions in. | |
| $smcFunc = array(); | |
| // This is our general bootstrap, a la SSI.php but with a few differences. | |
| unset ($db_show_debug); | |
| loadDatabase(); | |
| reloadSettings(); | |
| // Just in case there's a problem... | |
| set_error_handler('error_handler_cron'); | |
| $sc = ''; | |
| $_SERVER['QUERY_STRING'] = ''; | |
| $_SERVER['REQUEST_URL'] = FROM_CLI ? 'CLI cron.php' : $boardurl . '/cron.php'; | |
| // Now 'clean the request' (or more accurately, ignore everything we're not going to use) | |
| cleanRequest_cron(); | |
| // At this point we could reseed the RNG but I don't think we need to risk it being seeded *even more*. | |
| // Meanwhile, time we got on with the real business here. | |
| while ($task_details = fetch_task()) | |
| { | |
| $result = perform_task($task_details); | |
| if ($result) | |
| { | |
| $smcFunc['db_query']('', ' | |
| DELETE FROM {db_prefix}background_tasks | |
| WHERE id_task = {int:task}', | |
| array( | |
| 'task' => $task_details['id_task'], | |
| ) | |
| ); | |
| } | |
| } | |
| obExit_cron(); | |
| exit; | |
| /** | |
| * The heart of this cron handler... | |
| * @return bool|array False if there's nothing to do or an array of info about the task | |
| */ | |
| function fetch_task() | |
| { | |
| global $smcFunc; | |
| // Check we haven't run over our time limit. | |
| if (microtime(true) - TIME_START > MAX_CRON_TIME) | |
| return false; | |
| // Try to find a task. Specifically, try to find one that hasn't been claimed previously, or failing that, | |
| // a task that was claimed but failed for whatever reason and failed long enough ago. We should not care | |
| // what task it is, merely that it is one in the queue, the order is irrelevant. | |
| $request = $smcFunc['db_query']('cron_find_task', ' | |
| SELECT id_task, task_file, task_class, task_data, claimed_time | |
| FROM {db_prefix}background_tasks | |
| WHERE claimed_time < {int:claim_limit} | |
| ORDER BY null | |
| LIMIT 1', | |
| array( | |
| 'claim_limit' => time() - MAX_CLAIM_THRESHOLD, | |
| ) | |
| ); | |
| if ($row = $smcFunc['db_fetch_assoc']($request)) | |
| { | |
| // We found one. Let's try and claim it immediately. | |
| $smcFunc['db_free_result']($request); | |
| $smcFunc['db_query']('', ' | |
| UPDATE {db_prefix}background_tasks | |
| SET claimed_time = {int:new_claimed} | |
| WHERE id_task = {int:task} | |
| AND claimed_time = {int:old_claimed}', | |
| array( | |
| 'new_claimed' => time(), | |
| 'task' => $row['id_task'], | |
| 'old_claimed' => $row['claimed_time'], | |
| ) | |
| ); | |
| // Could we claim it? If so, return it back. | |
| if ($smcFunc['db_affected_rows']() != 0) | |
| { | |
| // Update the time and go back. | |
| $row['claimed_time'] = time(); | |
| return $row; | |
| } | |
| else | |
| { | |
| // Uh oh, we just missed it. Try to claim another one, and let it fall through if there aren't any. | |
| return fetch_task(); | |
| } | |
| } | |
| else | |
| { | |
| // No dice. Clean up and go home. | |
| $smcFunc['db_free_result']($request); | |
| return false; | |
| } | |
| } | |
| /** | |
| * This actually handles the task | |
| * @param array $task_details An array of info about the task | |
| * @return bool|void True if the task is invalid; otherwise calls the function to execute the task | |
| */ | |
| function perform_task($task_details) | |
| { | |
| global $sourcedir, $boarddir; | |
| // This indicates the file to load. | |
| if (!empty($task_details['task_file'])) | |
| { | |
| $include = strtr(trim($task_details['task_file']), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir)); | |
| if (file_exists($include)) | |
| require_once($include); | |
| } | |
| if (empty($task_details['task_class'])) | |
| { | |
| // This would be nice to translate but the language files aren't loaded for any specific language. | |
| log_error('Invalid background task specified (no class, ' . (empty($task_details['task_file']) ? ' no file' : ' to load ' . $task_details['task_file']) . ')'); | |
| return true; // So we clear it from the queue. | |
| } | |
| // All background tasks need to be classes. | |
| elseif (class_exists($task_details['task_class']) && is_subclass_of($task_details['task_class'], 'SMF_BackgroundTask')) | |
| { | |
| $details = empty($task_details['task_data']) ? array() : unserialize($task_details['task_data']); | |
| $bgtask = new $task_details['task_class']($details); | |
| return $bgtask->execute(); | |
| } | |
| else | |
| { | |
| log_error('Invalid background task specified: (class: ' . $task_details['task_class'] . ', ' . (empty($task_details['task_file']) ? ' no file' : ' to load ' . $task_details['task_file']) . ')'); | |
| return true; // So we clear it from the queue. | |
| } | |
| } | |
| // These are all our helper functions that resemble their big brother counterparts. These are not so important. | |
| /** | |
| * Cleans up the request variables | |
| * @return void | |
| */ | |
| function cleanRequest_cron() | |
| { | |
| global $scripturl, $boardurl; | |
| $scripturl = $boardurl . '/index.php'; | |
| // These keys shouldn't be set...ever. | |
| if (isset($_REQUEST['GLOBALS']) || isset($_COOKIE['GLOBALS'])) | |
| die('Invalid request variable.'); | |
| // Save some memory.. (since we don't use these anyway.) | |
| unset($GLOBALS['HTTP_POST_VARS'], $GLOBALS['HTTP_POST_VARS']); | |
| unset($GLOBALS['HTTP_POST_FILES'], $GLOBALS['HTTP_POST_FILES']); | |
| unset($GLOBALS['_GET'], $GLOBALS['_POST'], $GLOBALS['_REQUEST'], $GLOBALS['_COOKIE'], $GLOBALS['_FILES']); | |
| } | |
| /** | |
| * The error handling function | |
| * @param int $error_level One of the PHP error level constants (see ) | |
| * @param string $error_string The error message | |
| * @param string $file The file where the error occurred | |
| * @param int $line What line of the specified file the error occurred on | |
| * @return void | |
| */ | |
| function error_handler_cron($error_level, $error_string, $file, $line) | |
| { | |
| global $modSettings; | |
| // Ignore errors if we're ignoring them or they are strict notices from PHP 5 (which cannot be solved without breaking PHP 4.) | |
| if (error_reporting() == 0 || (defined('E_STRICT') && $error_level == E_STRICT && !empty($modSettings['enableErrorLogging']))) | |
| return; | |
| $error_type = 'cron'; | |
| log_error($error_level . ': ' . $error_string, $error_type, $file, $line); | |
| // If this is an E_ERROR or E_USER_ERROR.... die. Violently so. | |
| if ($error_level % 255 == E_ERROR) | |
| die('No direct access...'); | |
| } | |
| /** | |
| * The exit function | |
| */ | |
| function obExit_cron() | |
| { | |
| if (FROM_CLI) | |
| die(0); | |
| else | |
| { | |
| header('Content-Type: image/gif'); | |
| die("\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x21\xF9\x04\x01\x00\x00\x00\x00\x2C\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3B"); | |
| } | |
| } | |
| // We would like this to be defined, but we don't want to have to load more stuff than necessary. | |
| // Thus we declare it here, and any legitimate background task must implement this. | |
| /** | |
| * Class SMF_BackgroundTask | |
| */ | |
| abstract class SMF_BackgroundTask | |
| { | |
| protected $_details; | |
| /** | |
| * The constructor. | |
| * @param $details The details for the task | |
| */ | |
| public function __construct($details) | |
| { | |
| $this->_details = $details; | |
| } | |
| /** | |
| * The function to actually execute a task | |
| * @return mixed | |
| */ | |
| abstract public function execute(); | |
| } | |
| ?> |