} // // daily jobs // echo 'Checking daily jobs...' . BR; // get date of last run $record = Values::get_record('cron.daily', NULL_DATE); // wait at least 1 day = 86400 seconds between runs if (isset($record['edit_date'])) { $target = SQL::strtotime($record['edit_date']) + 86400; } else { $target = time(); } // request to be delayed if ($target > time()) { echo 'Wait until ' . gmdate('r', $target) . ' GMT' . BR; } else { Values::set('cron.daily', 'running...'); // do the job and provide feed-back to user $context['text'] = Hooks::include_scripts('daily'); echo $context['text']; // remember tick date and resulting text Values::set('cron.daily', $context['text']); // log outcome of script execution in debug mode if ($context['with_debug'] == 'Y') { Logger::remember('cron.php: daily processing', $context['text'], 'debug'); } } // all done $time = round(get_micro_time() - $context['start_time'], 2); exit(sprintf('Script terminated in %.2f seconds.', $time) . BR);
if ($recipients_ok == 0) { $context['text'] .= i18n::s('No letter has been transmitted.') . BR . "\n"; } elseif ($recipients_ok == 1) { $context['text'] .= i18n::s('One letter has been transmitted.') . BR . "\n"; } else { $context['text'] .= sprintf(i18n::s('%d letters have been transmitted.'), $recipients_ok) . BR . "\n"; } // transmission errors, if any if ($recipients_errors == 1) { $context['text'] .= i18n::s('One transmission error has been encountered.') . BR . "\n"; } elseif ($recipients_errors > 1) { $context['text'] .= sprintf(i18n::s('%d transmission errors have been encountered.'), $recipients_errors) . BR . "\n"; } // save digest stamp, if any if (isset($_REQUEST['digest_stamp']) && $_REQUEST['digest_stamp'] > NULL_DATE) { Values::set('letters.digest.stamp', $_REQUEST['digest_stamp']); } // display the execution time $time = round(get_micro_time() - $context['start_time'], 2); $context['text'] .= '<p>' . sprintf(i18n::s('Script terminated in %.2f seconds.'), $time) . '</p>'; // forward to the index page $menu = array('letters/' => i18n::s('Newsletters')); $context['text'] .= Skin::build_list($menu, 'menu_bar'); // make the user select an option } else { // the splash message $context['text'] .= '<p>' . i18n::s('This script will help you to prepare and to send a electronic message to community members. Please select below the action you would like to perform. Depending on your choice, the assistant may ask for additional parameters on successive panels.') . '</p>' . "\n"; // the form $context['text'] .= '<form method="get" action="' . $context['script_url'] . '" id="main_form">' . "\n"; // a digest of most recent articles $context['text'] .= '<p><input type="radio" name="action" value="digest" selected="selected" /> ' . i18n::s('Send a digest of articles published recently') . '</p>' . "\n";
/** * process new messages, if any * * This function checks inbound mailboxes, and process new messages on their arrival. * * This function is aiming to run silently, therefore errors are logged in a file. * * @return a string to be displayed in resulting page, if any */ public static function tick_hook() { global $context; // useless if we don't have a valid database connection if (!$context['connection']) { return; } // we need some queue definitions Safe::load('parameters/agents.include.php'); if (!isset($context['mail_queues']) || !is_array($context['mail_queues']) || !count($context['mail_queues'])) { return 'agents/messages.php: no queue has been defined' . BR; } // remember start time $stamp = get_micro_time(); // process each inbound queue include_once $context['path_to_root'] . 'shared/values.php'; // messages.tick $count = 0; foreach ($context['mail_queues'] as $name => $queue) { // count messages retrieved $messages = Messages::process_queue($queue); $count += $messages; // remember tick date Values::set('messages.tick.' . $name, $messages); } // rebuild index pages if ($count) { Cache::clear(); } // compute execution time $time = round(get_micro_time() - $stamp, 2); // report on work achieved if ($count > 1) { return 'agents/messages.php: ' . $count . ' messages have been processed (' . $time . ' seconds)' . BR; } elseif ($count == 1) { return 'agents/messages.php: 1 message has been processed (' . $time . ' seconds)' . BR; } else { return 'agents/messages.php: nothing to do (' . $time . ' seconds)' . BR; } }
/** * purge idle space * * This function OPTIMIZEs tables that may create overheads because of * frequent deletions, including: cache, links, members, messages, * notifications, values, versions, visits. * * Last purge is recorded as value 'sql.tick'. * * @param boolean optional TRUE to not report on any error * @return a string to be displayed in resulting page, if any */ public static function purge($silent = FALSE) { global $context; // useless if we don't have a valid database connection if (!$context['connection']) { return; } // remember start time $stamp = get_micro_time(); // get date of last tick include_once $context['path_to_root'] . 'shared/values.php'; $record = Values::get_record('sql.tick', NULL_DATE); // wait at least 8 hours = 24*3600 seconds between ticks if (isset($record['edit_date'])) { $target = SQL::strtotime($record['edit_date']) + 8 * 3600; } else { $target = time(); } // request to be delayed if ($target > time()) { return 'shared/sql.php: wait until ' . gmdate('r', $target) . ' GMT' . BR; } // recover unused bytes $query = 'OPTIMIZE TABLE ' . SQL::table_name('cache') . ', ' . SQL::table_name('links') . ', ' . SQL::table_name('members') . ', ' . SQL::table_name('messages') . ', ' . SQL::table_name('notifications') . ', ' . SQL::table_name('values') . ', ' . SQL::table_name('versions') . ', ' . SQL::table_name('visits'); $result = SQL::query($query, $silent); // remember tick date and resulting text Values::set('sql.tick', 'purge'); // compute execution time $time = round(get_micro_time() - $stamp, 2); // report on work achieved if ($result) { return 'shared/sql.php: unused bytes have been recovered (' . $time . ' seconds)' . BR; } else { return 'shared/sql.php: nothing to recover (' . $time . ' seconds)' . BR; } }
/** * process new uploads, if any * * This function checks the input queue, and process new files on their arrival. * * This function is aiming to run silently, therefore errors are logged in a file. * * @return a string to be displayed in resulting page, if any * */ public static function tick_hook() { global $context; // useless if we don't have a valid database connection if (!$context['connection']) { return; } // remember start time $stamp = get_micro_time(); // process handx weblog entries, if any $count = 0; if (($files = Uploads::list_files('inbox/entries')) && @count($files) > 0) { foreach ($files as $file) { // help the webmaster Logger::remember('agents/upload.php: processing ' . $file); // create articles Uploads::process_handx_weblog($file); // no more than 10 entries per tick $count += 1; if ($count >= 10) { break; } } // remember tick date include_once $context['path_to_root'] . 'shared/values.php'; Values::set('uploads.tick.entries', $count); } // rebuild index pages if ($count) { Cache::clear(); } // compute execution time $time = round(get_micro_time() - $stamp, 2); // report on work achieved if ($count > 1) { return 'agents/uploads.php: ' . $count . ' files have been processed (' . $time . " seconds)" . BR; } elseif ($count == 1) { return 'agents/uploads.php: 1 file has been processed (' . $time . " seconds)" . BR; } else { return 'agents/uploads.php: nothing to do (' . $time . " seconds)" . BR; } }
/** * process deferred messages * * Most often, the server has to stay below a given rate of messages, * for example 50 messages per hour. * * Of course, any lively community will feature bursts of activity and of * messages, therefore the need for a shaping mechanism. * * YACS implements a leaking bucket algorithm to take care of messages sent * previously: * * 1. Initially, the bucket is empty. * * 2. New messages are queued in the database, to be processed asynchronously. * * 3. On background ticks, the bucket is decremented. If the bucket becomes * empty, and if some messages have been queued, a couple of them are sent, and * the bucket is incremented accordingly. * * Bucket content is managed as value 'bucket.content' saved in the database. * * The bucket size is given by parameter $context['mail_hourly_maximum'], set * in the configuration panel for system parameters. * * This parameter has a default value of 50, meaning YACS will not send more * than 50 messages per hour. * * Background processing is either added to regular page generation or delegated * to an external sub-system (e.g., cron). In case of a large site, we recommend * to use the second solution, even if this adds additional setup steps. Your * choice will be recorded in the configuration panel for system parameters. * * @see control/configure.php * * The number of messages sent on each tick can go up to the bucket size if * background processing is external. Else it is one fourth of bucket size, to * minimize impact on watching surfer. * * @see cron.php */ public static function tick_hook() { global $context; // email services have to be activated if (!isset($context['with_email']) || $context['with_email'] != 'Y') { return; } // useless if we don't have a valid database connection if (!$context['connection']) { return; } // remember start time $start = get_micro_time(); // get bucket size --force it if set to 0 if (!isset($context['mail_hourly_maximum']) || $context['mail_hourly_maximum'] < 5) { $context['mail_hourly_maximum'] = 50; } // get record related to last tick include_once $context['path_to_root'] . 'shared/values.php'; $bucket = Values::get_record('mailer.bucket.content', 0); $bucket['value'] = intval($bucket['value']); // some content to leak if ($bucket['value'] > 0) { // date of last stamp if (isset($bucket['edit_date'])) { $stamp = SQL::strtotime($bucket['edit_date']); } else { $stamp = time() - 3600; } // leak is maximum after one hour $leak = intval($context['mail_hourly_maximum'] * (time() - $stamp) / 3600); // preserve previous value until actual leak if ($leak < 1) { return; } // actual leak $bucket['value'] = max(0, $bucket['value'] - $leak); } // process some messages only when bucket is empty $count = 0; if ($bucket['value'] < 1) { // reduced speed if on-line processing if (isset($_SERVER['REMOTE_ADDR'])) { $slice = intval($context['mail_hourly_maximum'] / 4); } else { $slice = intval($context['mail_hourly_maximum']); } // get some messages, if any $query = "SELECT * FROM " . SQL::table_name('messages') . " ORDER BY edit_date LIMIT 0, " . $slice; if ($result = SQL::query($query)) { // process every message while ($item = SQL::fetch($result)) { Mailer::process($item['recipient'], $item['subject'], $item['message'], $item['headers']); // purge the queue $query = 'DELETE FROM ' . SQL::table_name('messages') . ' WHERE id = ' . $item['id']; SQL::query($query); // fill the bucket $bucket['value'] += 1; $count++; // take care of time if (!($count % 50)) { // ensure enough execution time Safe::set_time_limit(30); } } // close connection Mailer::close(); } } // remember new state of the bucket Values::set('mailer.bucket.content', $bucket['value']); // compute execution time $time = round(get_micro_time() - $start, 2); // report on work achieved if ($count > 1) { return 'shared/mailer.php: ' . $count . ' messages have been processed (' . $time . ' seconds)' . BR; } elseif ($count == 1) { return 'shared/mailer.php: 1 message has been processed (' . $time . ' seconds)' . BR; } elseif ($bucket['value']) { return 'shared/mailer.php: delaying messages (' . $time . ' seconds)' . BR; } else { return 'shared/mailer.php: nothing to do (' . $time . ' seconds)' . BR; } }
/** * get news from remote servers * * This function queries remote sources and populate the table of links based on fetched news. * * On tick, the including hook calls [code]Feeds::tick_hook()[/code]. * See [script]control/scan.php[/script] for a more complete description of hooks. * * The function browses the database to locate servers acting as feeders, and read the URLs to use. * * A round-robin algorithm is implemented, meaning that servers are polled in sequence throughout successive ticks. * At most 1 feed is parsed on each tick, to limit impact when the "poor-man" cron mechanism is used, * which is the default setting. * * XML feeds are fetched and parsed according to their type. * At the moment YACS is able to process RSS and slashdot feeds. * Link records are created or updated in the database saving as much of possible of provided data. * Item data is reflected in Link, Title, and Description fields. * Channel data is used to populate the Source field. * Stamping information is based on feeding date, and channel title. * Also, the edit action 'link:feed' marks links that are collected from feeders. * The anchor field is set to the category assigned in the server profile. * * At the end of the feeding process, the database is purged from oldest links according to the limit * defined in parameters/feeds.include.php, set through feeds/configure.php. * See Links::purge_old_news(). * * @param boolean if set to true, fetch news on each call; else use normal period of time * @return a string to be displayed in resulting page, if any * * @see control/scan.php * @see feeds/configure.php */ public static function tick_hook($forced = FALSE) { global $context; // load librairies only once include_once $context['path_to_root'] . 'links/links.php'; include_once $context['path_to_root'] . 'servers/servers.php'; include_once $context['path_to_root'] . 'shared/values.php'; // feeds.tick // get feeding parameters Safe::load('parameters/feeds.include.php'); // delay between feeds - minimum is 5 minutes if (!isset($context['minutes_between_feeds']) || $context['minutes_between_feeds'] < 5) { $context['minutes_between_feeds'] = 5; } // do not wait for the end of a feeding cycle if ($forced) { $threshold = gmstrftime('%Y-%m-%d %H:%M:%S'); } else { $threshold = gmstrftime('%Y-%m-%d %H:%M:%S', time() - $context['minutes_between_feeds'] * 60); } // get a batch of feeders if (!($feeders = Servers::list_for_feed(0, 1, 'feed'))) { return 'feeds/feeds.php: no feed has been defined' . BR; } // remember start time $start_time = get_micro_time(); // list banned tokens $banned_pattern = Servers::get_banned_pattern(); // browse each feed $count = 0; foreach ($feeders as $server_id => $attributes) { // get specific feed parameters list($feed_url, $feed_title, $anchor, $stamp) = $attributes; // skip servers processed recently if ($stamp > $threshold) { continue; } // flag this record to enable round-robin even on error Servers::stamp($server_id); // fetch news from the provided link if (!($news = Feeds::get_remote_news_from($feed_url)) || !is_array($news)) { continue; } // no anchor has been defined for this feed if (!$anchor) { // create a default section if necessary if (!($anchor = Sections::lookup('external_news'))) { $fields = array(); $fields['nick_name'] = 'external_news'; $fields['create_date'] = gmstrftime('%Y-%m-%d %H:%M:%S', time()); $fields['edit_date'] = gmstrftime('%Y-%m-%d %H:%M:%S', time()); $fields['index_map'] = 'N'; $fields['locked'] = 'Y'; // no direct contributions $fields['rank'] = 40000; // at the end of the list $fields['title'] = i18n::c('External News'); $fields['description'] = i18n::c('Received from feeding servers'); if (!($fields['id'] = Sections::post($fields))) { Logger::remember('feeds/feeds.php: Impossible to add a section.'); return; } $anchor = 'section:' . $fields['id']; } } // process retrieved links $links = 0; foreach ($news as $item) { // link has to be valid if (!isset($item['link']) || !($item['title'] . $item['description'])) { if (isset($context['debug_feeds']) && $context['debug_feeds'] == 'Y') { Logger::remember('feeds/feeds.php: feed item is invalid', $item, 'debug'); } continue; } // skip banned servers if ($banned_pattern && preg_match($banned_pattern, $item['link'])) { if (isset($context['debug_feeds']) && $context['debug_feeds'] == 'Y') { Logger::remember('feeds/feeds.php: feed host has been banned', $item['link'], 'debug'); } continue; } // one link processed $links++; // link description $fields = array(); $fields['anchor'] = $anchor; $fields['link_url'] = $item['link']; $fields['title'] = $item['title']; $fields['description'] = $item['description']; if ($item['category']) { $fields['description'] .= ' (' . $item['category'] . ')'; } $fields['edit_name'] = $feed_title; $fields['edit_address'] = $feed_url; $fields['edit_action'] = 'link:feed'; if ($item['pubDate']) { $fields['edit_date'] = gmstrftime('%Y-%m-%d %H:%M:%S', strtotime($item['pubDate'])); } // update links that already exist in the database if (Links::have($item['link'], $anchor, $fields)) { continue; } // save link in the database if (!Links::post($fields)) { Logger::remember('feeds/feeds.php: Impossible to save feed link: ' . Logger::error_pop()); } } // one feed has been processed $count += 1; // remember tick date Values::set('feeds.tick.' . $feed_url, $links); } // cap the number of links used for news if (!isset($context['maximum_news']) || !$context['maximum_news']) { $context['maximum_news'] = 1000; } if ($context['maximum_news'] > 10) { include_once $context['path_to_root'] . 'links/links.php'; Links::purge_old_news($context['maximum_news']); } // compute execution time $time = round(get_micro_time() - $start_time, 2); // report on work achieved if ($count > 1) { return 'feeds/feeds.php: ' . $count . ' feeds have been processed (' . $time . ' seconds)' . BR; } elseif ($count == 1) { return 'feeds/feeds.php: 1 feed has been processed (' . $time . ' seconds)' . BR; } else { return 'feeds/feeds.php: nothing to do (' . $time . ' seconds)' . BR; } }