mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 01:08:56 +00:00
2546 lines
72 KiB
PHP
2546 lines
72 KiB
PHP
<?php
|
|
|
|
use newznab\db\Settings;
|
|
|
|
// Help out those who don't have SSL enabled
|
|
if(!defined('OPENSSL_KEYTYPE_RSA')) {
|
|
// OPENSSL_KEYTYPE_RSA is defined as 0 in php v4 and v5
|
|
// so just give it a value to silence possible Notice Errors
|
|
// for Windows Users (and give it the correct value)
|
|
define('OPENSSL_KEYTYPE_RSA', 0);
|
|
}
|
|
|
|
// Define OpenSSL Config File
|
|
define('OPENSSL_CFG_PATH', WWW_DIR.'/lib/openssl/openssl.cnf');
|
|
|
|
// JSON Encode Support (for those missing the constants)
|
|
if(!defined('JSON_HEX_TAG')) define('JSON_HEX_TAG', 1);
|
|
if(!defined('JSON_HEX_AMP')) define('JSON_HEX_AMP', 2);
|
|
if(!defined('JSON_HEX_APOS')) define('JSON_HEX_APOS', 4);
|
|
if(!defined('JSON_HEX_QUOT')) define('JSON_HEX_QUOT', 8);
|
|
if(!defined('JSON_UNESCAPED_UNICODE')) define('JSON_UNESCAPED_UNICODE', 256);
|
|
|
|
// Silent Error Handler (used to shut up noisy XML exceptions)
|
|
// we use the silent error handler for remote connection failures as well
|
|
function snHandleError($errno, $errstr, $errfile, $errline, array $errcontext){
|
|
if (0 === error_reporting())
|
|
return false;
|
|
if(!defined('E_STRICT'))define('E_STRICT', 2048);
|
|
switch($errno){
|
|
case E_WARNING:
|
|
case E_NOTICE:
|
|
case E_STRICT:
|
|
return;
|
|
default:
|
|
break;
|
|
};
|
|
throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
|
|
}
|
|
|
|
// Create a NNTP Exception type so we can identify it from others
|
|
class SpotNabException extends \Exception { }
|
|
|
|
class SpotNab {
|
|
// Segment Identifier domain is used to help build segments
|
|
// prior to them being posted.
|
|
const SEGID_DOMAIN = 'sample.com';
|
|
|
|
// Subject line can look like this:
|
|
// 01c9478809c80ccb07246d19852ed33b0a5f5d8d-20130125030511
|
|
// 517a6210bd588e964654f75b807d65d55d420f5e-20130125002943
|
|
// 33012186754c9c75050848d825ad6c2ae1af2e4d-20130125002849
|
|
// To speed up fetch process, we can parse this to determine wether
|
|
// or not to continue or not.
|
|
|
|
// The TYPE is always prefixed on the subject, in this case, all
|
|
// Comment types are always 00
|
|
const FETCH_COMMENT_TYPE = "00";
|
|
const FETCH_COMMENT_SUBJECT_REGEX =
|
|
'/^(?P<checksum>[0-9a-z]{40})-(?P<utcref>[0-9]{14})$/i';
|
|
|
|
// Discovery types are always 01
|
|
const FETCH_DISCOVERY_TYPE = "01";
|
|
const FETCH_DISCOVERY_SUBJECT_REGEX =
|
|
'/^(?P<checksum>[0-9a-z]{40})-(?P<utcref>[0-9]{14})$/i';
|
|
|
|
// The Message id can be parsed as follows
|
|
const FETCH_MSGID_REGEX =
|
|
'/^<(?P<crap>[a-z0-9]{30})\.(?P<type>[0-9]{2})\.(?P<local>[0-9]+)[^@]*@(?P<domain>.*)>$/i';
|
|
|
|
// How many consecutive misses in a row do we allow while trying to retrieve
|
|
// historic messages do we allow before assuming that we've exceeded the
|
|
// the retention area. In which case we just return the last date we
|
|
// matched before the miss count started
|
|
const FETCH_MAX_MISSES = 15;
|
|
|
|
// Maximum number of messages to process at one time
|
|
// Setting this to too large of a value can cause your usenet to
|
|
// ignore and deny your request.
|
|
const FETCH_USENET_BATCH = 25000;
|
|
|
|
// Maximum number of messages to look back if one source
|
|
// is lookin like it hasn't posted anything in a very long
|
|
// time... we stop counting back headers when we reached
|
|
// this figure
|
|
const FETCH_MAXIMUM_HEADERS = 200000;
|
|
|
|
// Maximum age (in seconds) we look back for a source
|
|
const FETCH_MAXIMUM_AGE = 172800;
|
|
|
|
// The maximum number of comments that can exist within 1 post to usenet
|
|
const POST_MAXIMUM_COMMENTS = 500;
|
|
|
|
// The number of seconds to wait before sending a new broadcast
|
|
const POST_BROADCAST_INTERVAL = 2592000;
|
|
|
|
// Verify Fetch Range; This is the number of records to look back after
|
|
// a post to be sure that the post was successful. The number may appear
|
|
// kind of high, but consider a large active group and
|
|
// this number makes sense. Plus it doesn't take that long to rescan
|
|
// a few headers.
|
|
const VERIFY_FETCH_HEADER_COUNT = 300;
|
|
|
|
// The php function openssl_public_encrypt() seems to fail whenever it's
|
|
// passed more then this many characters into its buffer, therefore
|
|
// we need to encrypt in batches if the content is longer or we
|
|
// fail hard. DO NOT CHANGE THIS VALUE!! - EVER
|
|
const SSL_MAX_BUF_LEN = 117;
|
|
|
|
// If SSL_MAX_BUF_LEN is to large, we need to use delimiters
|
|
// to help separate the batches as they are processed
|
|
const SSL_BUF_DELIMITER = "\n";
|
|
|
|
// Auto Discovery Functionality
|
|
// Autodisovery allows us to post encrypted information other potential
|
|
// sources we can use. We also post our source information so others can
|
|
// refer to our comments as well.
|
|
const AUTODISCOVER_POST_GROUP = "alt.binaries.aac";
|
|
|
|
const AUTODISCOVER_POST_USER = "auto";
|
|
|
|
const AUTODISCOVER_POST_EMAIL = "auto@ohnohs.ru";
|
|
|
|
protected $_nntp;
|
|
private $_site;
|
|
protected $_globals;
|
|
|
|
// Meta Information is fetched from DB
|
|
private $_post_site;
|
|
private $_post_code;
|
|
private $_post_privacy;
|
|
private $_post_title;
|
|
|
|
private $_post_user;
|
|
private $_post_email;
|
|
private $_post_group;
|
|
|
|
/* SSL Public & Private Keys */
|
|
private $_ssl_pubkey;
|
|
private $_ssl_prvkey;
|
|
|
|
/* SSL Auto Discovery Public & Private Keys */
|
|
protected $_ssl_auto_pubkey;
|
|
protected $_ssl_auto_prvkey;
|
|
|
|
/* Some booleans to make code reading easier */
|
|
private $_can_post;
|
|
private $_can_discover;
|
|
private $_can_broadcast;
|
|
private $_auto_enable;
|
|
|
|
/* Track the last article scanned when preforming a discovery */
|
|
private $_discovery_lastarticle;
|
|
|
|
public function __construct($post_user = Null, $post_email = Null, $post_group = Null) {
|
|
$this->_pdo = new Settings();
|
|
$this->_nntp = new NNTP(['Settings' => $this->_pdo]);
|
|
$this->releaseImage = new \ReleaseImage($this->_pdo);
|
|
$this->nzb = new \NZB($this->_pdo);
|
|
$s = new Sites();
|
|
$this->_site = $s->get();
|
|
$this->_globals = $this->_site;
|
|
|
|
$this->_post_user = $post_user;
|
|
$this->_post_email = $post_email;
|
|
$this->_post_group = $post_group;
|
|
|
|
// Fetch Meta information
|
|
$this->_post_code = trim($this->_globals->code)?
|
|
$this->_globals->code:Null;
|
|
$this->_post_title = trim($this->_globals->title)?
|
|
$this->_globals->title:Null;
|
|
|
|
if ($this->_post_user === Null){
|
|
// Fetch the SpotNab UserID
|
|
$this->_post_user = trim($this->_globals->spotnabuser)?
|
|
$this->_globals->spotnabuser:Null;
|
|
}
|
|
|
|
if ($this->_post_email === Null){
|
|
// Fetch the SpotNab EmailID
|
|
$this->_post_email = trim($this->_globals->spotnabemail)?
|
|
$this->_globals->spotnabemail:Null;
|
|
}
|
|
|
|
if ($this->_post_group === Null){
|
|
// Fetch the SpotNab Usenet Group
|
|
$this->_post_group = trim($this->_globals->spotnabgroup)?
|
|
$this->_globals->spotnabgroup:Null;
|
|
}
|
|
|
|
// Public Key
|
|
$this->_ssl_pubkey = trim($this->_pdo->getSetting('spotnabsitepubkey'))?
|
|
$this->_pdo->getSetting('spotnabsitepubkey') : false;
|
|
if($this->_ssl_pubkey)
|
|
$this->_ssl_pubkey = $this->decompstr($this->_ssl_pubkey);
|
|
|
|
// Private Key
|
|
$this->_ssl_prvkey = trim($this->_pdo->getSetting('spotnabsiteprivkey'))?
|
|
$this->_pdo->getSetting('spotnabsiteprivkey') : false;
|
|
if($this->_ssl_prvkey)
|
|
$this->_ssl_prvkey = $this->decompstr($this->_ssl_prvkey);
|
|
|
|
// Track Discovery Article
|
|
$this->_discovery_lastarticle = intval($this->_pdo->getSetting('spotnablastarticle'));
|
|
|
|
// Posting Flag
|
|
$this->_can_post = (trim($this->_pdo->getSetting('spotnabpost')) == 1)?
|
|
true:false;
|
|
|
|
// Auto Enable Flag
|
|
$this->_auto_enable = (trim($this->_pdo->getSetting('spotnabautoenable')) == 1)?
|
|
true:false;
|
|
|
|
// Spotnab Privacy Posting
|
|
$this->_post_privacy = (trim($this->_pdo->getSetting('spotnabsiteprivacy')) == 1)?
|
|
true:false;
|
|
|
|
// Auto-Discovery Private Key (used for Posting)
|
|
$this->_ssl_auto_prvkey = "eJxtk7mOo0AARHO+YnI0Moe5woYGppv7PjLABmzAgDFg"
|
|
."/PU7u/FWWlJJT6X3/f0bWdWR/eUH4Mv1UQxC9ctQs7/FN2EhpKQekgEw5MZTTi+Kt"
|
|
."9pujGz84DeF8xeLY2vXODzDIbvERbWEs4TFGqebjAeXlXjb8Qa7kHrd6P1Ie646ng"
|
|
."DBmYU1zOmZmrJE+7j4yCjf2XgDxbpc4NlVteGVikgTCyLb4z7Q1fPakDGXinjqKkE"
|
|
."riorE1dzA/FHSil5Eu4/yNbRu0UMXlUF7OxLeEQQekAkw6jLAL9gmaRvKca7pyy4D"
|
|
."S9zLoWx4LP/Q67EKaluREVSG8GzXdLOdaVHk2TZE4odQaOQNkc+8nGCxEzWQIsBQk"
|
|
."GNV6vr8fD4K3bpzdrqLG0zNZkhLvxyi5Urz6w5v/Xw+iLW/to9Zv5SqA6kkNbTYg5"
|
|
."Jl66Em2u2cvvAx2EWSM3JxWyiNa0GnAikzHkZdaA4pSIRDlYO0OE8Xf/rkTPth46d"
|
|
."vuTaXPGx20TKXNBszaIKHhl3bAsGJ1zuW3DSGa6lB22Oi1nM0P6al2vEvej16m96i"
|
|
."4/QDynQF49oIw4QaAMiBbYO+T19Xw8NVuyZ9K9zeXKgSMnifYIXbe/PBjIHuvHWTs"
|
|
."/BqUQFJKXy6UiyleDpv5Wo0i+1lnATEps7RVcut8OMgIEyuSRcU90d+rNTGh3g3Yc"
|
|
."/zUyHQLDlxjTazOx2bCZWZqxxgm0PlpRREyAa/w55CEre7Go2DNxWkC9vRVHqhflR"
|
|
."NVduIZhOoJOdho9jeZRjxMtAuNae29fNze0p2Zr6PbFKJJezphdnOgXE+CrFJLq+S"
|
|
."A528knKChLjTlKovVc3gJ5fpX6F8uY/syDXPg/xsu9wpKsEvmznLcENOVUXSVXova"
|
|
."pt/CnrA5O8BuOqAbvAO/Qy8d0j800e14f+1+gPs3xif";
|
|
|
|
// Auto-Discovery Public Key (used for discovering)
|
|
$this->_ssl_auto_pubkey = "eJxlz7lygkAAgOGep7BnGCEIwXLv2UWOBTk7ZAZkUCKR"
|
|
."Q/L0Odr85d99mvYTJIz7uzCBJ452Lil+n6Z4nDUe0BmKRxbzi4klgUAmAByYDzCCn"
|
|
."XRhK9F+0m3v2n8kvhjsBVnR07PMJnQ3RbqB2mchb46iyExBLXZ6k/g5v/x0wf1Znz"
|
|
."pj3fKbVW+fgOPRxA0ujVF/FBn9CsVWKHoULLbLUwYrMYaE3qfc4dSpijW9xYwc5lZ"
|
|
."NrdwRj75+p1VVq6IeW1wOFwOxKlkjhZfz2euSgTnoTl/BUawcAwmg8ickPv6H/gbI"
|
|
."LVDl";
|
|
|
|
// Auto-Discovery Flags
|
|
$this->_can_broadcast = (trim($this->_globals->spotnabbroadcast) == 1)?
|
|
true:false;
|
|
$this->_can_broadcast = ($this->_can_broadcast && $this->_can_post);
|
|
|
|
$this->_can_discover = (trim($this->_globals->spotnabdiscover) == 1)?
|
|
true:false;
|
|
|
|
if (!$this->has_openssl()){
|
|
// Can SpotNab even work; if not, we disable all flags
|
|
$this->_can_broadcast = false;
|
|
$this->_can_post = false;
|
|
}
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function has_openssl(){
|
|
// return true if ssl is correctly configured and installed
|
|
// otherwise return a fail
|
|
return (is_readable(OPENSSL_CFG_PATH) && extension_loaded("openssl"));
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function auto_clean($max_days=90){
|
|
$db = new Settings();
|
|
// automatically sweep old sources lingering that have not shown any
|
|
// sort of life what-so-ever for more then 90 days
|
|
$sql = "DELETE FROM spotnabsources WHERE "
|
|
."lastbroadcast IS NOT NULL AND "
|
|
."lastbroadcast < NOW() - INTERVAL $max_days DAY";
|
|
$res = $db->queryExec($sql);
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function orphan_comment_clean($max_days=1, $batch=500){
|
|
// Clean out orphan comments that are older then at least 1 day
|
|
// this is to address people who do not wish to hold on to
|
|
// comments they do not have a release for... Makes sense :)
|
|
$offset = 0;
|
|
$sql = "SELECT DISTINCT(gid) as gid FROM releasecomment "
|
|
."WHERE releaseid = 0 "
|
|
."AND createddate < NOW() - INTERVAL $max_days DAY "
|
|
."ORDER BY createddate "
|
|
."LIMIT %d,%d";
|
|
|
|
$sql_rel = "SELECT gid FROM releases WHERE gid IN ('%s') ";
|
|
$sql_del = "DELETE FROM releasecomment WHERE gid IN ('%s')";
|
|
$total_delcnt = 0;
|
|
$db = new Settings();
|
|
while(1)
|
|
{
|
|
$res = $db->query(sprintf($sql, $offset, $batch));
|
|
if(!$res)break;
|
|
|
|
# Assemble results into list
|
|
$gids_found = array();
|
|
$gids_matched = array();
|
|
foreach($res as $item)
|
|
$gids_found[] = $item['gid'];
|
|
|
|
#echo 'B:'.sprintf($sql_rel, implode("','", $gids_found))."\n";
|
|
$res2 = $db->query(sprintf($sql_rel, implode("','", $gids_found)));
|
|
foreach($res2 as $item)
|
|
$gids_matched[] = $item['gid'];
|
|
# Now we want to create an inverted list by eliminating the
|
|
# matches we just fetched
|
|
$gids_missing = array_diff($gids_found, $gids_matched);
|
|
//print_r($gids_missing);
|
|
if(count($gids_missing)){
|
|
$s_gids_missing = implode("','", $gids_missing);
|
|
$dresc = $db->queryExec(sprintf($sql_del, $s_gids_missing));
|
|
$total_delcnt += count($gids_missing);
|
|
$offset += $batch - count($gids_missing);
|
|
}else{
|
|
$offset += $batch;
|
|
}
|
|
# make noise
|
|
echo '.';
|
|
|
|
if(!count($res))break;
|
|
}
|
|
return $total_delcnt;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function soft_reset(){
|
|
// A harmless function that resets spotnab without losing sources
|
|
// Calling this function will reset spotnab to think:
|
|
// - it hasn't fetched anything yet from existing sources
|
|
// - it needs to gracefully build a starting point from scratch
|
|
// using existing sources
|
|
// - it has never posted discovery information
|
|
// - it has never scanned for existing discoveries
|
|
$db = new Settings();
|
|
|
|
// resets sources so they need to query again
|
|
$sources = "UPDATE spotnabsources SET "
|
|
."lastupdate = NULL,"
|
|
."lastbroadcast = NULL,"
|
|
."lastarticle = 0";
|
|
$discovery_a = "UPDATE settings SET "
|
|
."value = '0' "
|
|
."WHERE setting = 'spotnablastarticle'";
|
|
$broadcast = "UPDATE settings SET "
|
|
."updateddate = '1980-01-01 00:00:00' "
|
|
."WHERE setting = 'spotnabbroadcast'";
|
|
|
|
// Discovery should only be set back X days worth defined
|
|
// by the maximum age a broadcast can be.
|
|
$reftime = date("Y-m-d H:i:s",
|
|
time()-(SpotNab::POST_BROADCAST_INTERVAL));
|
|
$discovery_b = "Update settings SET "
|
|
."updateddate = '1980-01-01 00:00:00' "
|
|
."WHERE setting = 'spotnabdiscover'";
|
|
|
|
$post = "Update settings SET "
|
|
."updateddate = '$reftime' "
|
|
."WHERE setting = 'spotnabpost'";
|
|
$db->queryExec($sources);
|
|
$db->queryExec($discovery_a);
|
|
$db->queryExec($discovery_b);
|
|
$db->queryExec($broadcast);
|
|
$db->queryExec($post);
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function fetch_discovery($reftime=Null, $retries=3){
|
|
$db = new Settings();
|
|
$last = $first = Null;
|
|
|
|
// Return Value; Initialize it to Okay
|
|
// we'll change it to false if we have to.
|
|
$fetch_okay = true;
|
|
|
|
// Track how many records were inserted, updated
|
|
$inserted = 0;
|
|
$updated = 0;
|
|
|
|
if (!$this->_can_discover){
|
|
// discovery disabled
|
|
return false;
|
|
}
|
|
|
|
if($reftime === Null){
|
|
$q = "SELECT updateddate FROM settings WHERE "
|
|
."setting = 'spotnabdiscover'";
|
|
$res = $db->queryOneRow($q);
|
|
if($res){
|
|
$reftime = $res['updateddate'];
|
|
}else{
|
|
// Fetch local time (but look back the maximum duration
|
|
// that a discovery message can exist for
|
|
$reftime = $this->utc2local((time()-(SpotNab::POST_BROADCAST_INTERVAL)));
|
|
}
|
|
}
|
|
|
|
// Now we fetch headers
|
|
set_error_handler('snHandleError');
|
|
|
|
// Connect to server
|
|
try{
|
|
if (($this->_site->alternate_nntp == 1 ? $this->_nntp->doConnect(true, true) : $this->_nntp->doConnect()) !== true) {
|
|
exit($this->_pdo->log->error("Unable to connect to usenet." . PHP_EOL));
|
|
}
|
|
}
|
|
catch(Exception $e){
|
|
printf("Failed to connect to Usenet\n");
|
|
// Restore handler
|
|
restore_error_handler();
|
|
return false;
|
|
}
|
|
|
|
echo "Spotnab : Discovery ";
|
|
$summary = $this->_nntp->selectGroup(
|
|
SpotNab::AUTODISCOVER_POST_GROUP);
|
|
while(NNTP::isError($summary))
|
|
{
|
|
// Reset Connection (This happens first time
|
|
// around since _nntpReset() will perform
|
|
// the initial connect if it hasn't been already
|
|
$summary = $this->_nntpReset(
|
|
SpotNab::AUTODISCOVER_POST_GROUP);
|
|
|
|
// Track retry attempts
|
|
$retries--;
|
|
if($retries <= 0){
|
|
// Retry Atempts exausted
|
|
echo "Failed\n";
|
|
// Restore handler
|
|
restore_error_handler();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$first = $this->_discovery_lastarticle;
|
|
if($first <= 0 || $first > $summary['last'] ){
|
|
// Look back until reftime
|
|
$first = $this->_first_article_by_date(
|
|
SpotNab::AUTODISCOVER_POST_GROUP,
|
|
$reftime
|
|
);
|
|
}
|
|
|
|
if($first === false){
|
|
// Fail
|
|
echo "Failed\n";
|
|
restore_error_handler();
|
|
return false;
|
|
}
|
|
|
|
// Group Processing Initialization
|
|
$processed = 0;
|
|
$batch = $last = intval($summary['last']);
|
|
$total = abs($last-$first);
|
|
|
|
// Select Group
|
|
while($fetch_okay && $processed < $total)
|
|
{
|
|
try
|
|
{
|
|
// Prepare Initial Batch
|
|
if ($total > SpotNab::FETCH_USENET_BATCH)
|
|
$batch = $first + SpotNab::FETCH_USENET_BATCH;
|
|
|
|
// Batch Processing
|
|
while ($processed < $total)
|
|
{
|
|
$headers = $this->_get_headers(SpotNab::AUTODISCOVER_POST_GROUP,
|
|
"$first-$batch", $retries);
|
|
|
|
if($headers === false){
|
|
// Retry Atempts exausted
|
|
$fetch_okay = false;
|
|
break;
|
|
}
|
|
|
|
// Process the header batch
|
|
$saved = $this->process_discovery_headers($headers);
|
|
if($saved !== false)
|
|
{
|
|
$inserted += $saved[0];
|
|
$updated += $saved[1];
|
|
}
|
|
|
|
$processed += ($batch-$first);
|
|
// Increment starting index
|
|
$first += ($batch-$first);
|
|
|
|
if ($last-$first >= SpotNab::FETCH_USENET_BATCH){
|
|
// Fetch next batch
|
|
$batch = $first + SpotNab::FETCH_USENET_BATCH;
|
|
}else{
|
|
$batch = $last;
|
|
}
|
|
//echo "$first-$batch, processed=$processed\n";
|
|
//print_r($headers);
|
|
}
|
|
|
|
}catch(Exception $e){
|
|
// Reset Connection
|
|
$fetch_okay = $this->_nntpReset(SpotNab::AUTODISCOVER_POST_GROUP);
|
|
|
|
// Track retry attempts
|
|
$retries--;
|
|
if($retries <= 0){
|
|
// Retry Atempts exausted
|
|
$fetch_okay = false;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
$sql = sprintf("Update settings SET value = '%d' "
|
|
."WHERE setting = 'spotnablastarticle'", $last);
|
|
$db->queryExec($sql);
|
|
printf("%d new and %d updated source(s).\n", $inserted, $updated);
|
|
|
|
// Update reference point
|
|
$q = "Update settings SET updateddate = NOW() WHERE "
|
|
."setting = 'spotnabdiscover'";
|
|
$db->queryExec($q);
|
|
|
|
// Restore handler
|
|
restore_error_handler();
|
|
return $inserted + $updated;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function auto_post_discovery($repost_sec=SpotNab::POST_BROADCAST_INTERVAL){
|
|
$db = new Settings();
|
|
// performs a post discovery once the time in seconds has elapsed
|
|
$q = "SELECT updateddate FROM settings WHERE "
|
|
."setting = 'spotnabbroadcast'";
|
|
$res = $db->queryOneRow($q);
|
|
$then = strtotime($res['updateddate']);
|
|
$now = time();
|
|
if(($now - $then) > $repost_sec){
|
|
// perform a post
|
|
if($this->post_discovery())
|
|
{
|
|
// Update post time
|
|
$q = "Update settings SET updateddate = NOW() WHERE "
|
|
."setting = 'spotnabbroadcast'";
|
|
$res = $db->queryExec($q);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function post_discovery($reftime=Null, $retries=3){
|
|
$reftime_local = $reftime;
|
|
$article = Null;
|
|
$rc = new ReleaseComments();
|
|
$us = new Users();
|
|
|
|
if($reftime_local === Null){
|
|
// Fetch local time
|
|
$reftime_local = $this->utc2local();
|
|
}
|
|
// some error checking....
|
|
if(!$this->_can_broadcast){
|
|
// Broadcasting not possible
|
|
return false;
|
|
}
|
|
|
|
// Generate keys if one doesn't exist
|
|
if(!($this->_ssl_prvkey && $this->_ssl_pubkey))
|
|
if($this->keygen(false, true) === false)
|
|
return false;
|
|
|
|
// Get Discovery Private Key
|
|
$prvkey = $this->decompstr($this->_ssl_auto_prvkey);
|
|
if (!$prvkey){
|
|
// This is a serious problem because the hard-coded discovery
|
|
// key should always decrypt!
|
|
return false;
|
|
}
|
|
|
|
printf("Spotnab : Broadcast ...");
|
|
|
|
// Fetch some date ranges
|
|
$last_month = date("Y-m-d",strtotime(
|
|
date("Y-m-d", time()) . " - 30 day"));
|
|
$last_year = date('Y-m-d',strtotime(
|
|
date("Y-m-d", time()) . " - 365 day"));
|
|
|
|
// Header
|
|
$message = array(
|
|
'site' => array(
|
|
// title & code taken out to keep things anonymous for now
|
|
//'title' => $this->_post_title,
|
|
//'code' => $this->_post_code,
|
|
'id' => md5($this->_globals->siteseed),
|
|
'users' => $us->getCount()
|
|
),
|
|
'posts' => array(
|
|
'user' => $this->_post_user,
|
|
'email' => $this->_post_email,
|
|
'group' => $this->_post_group,
|
|
'privacy' => $this->_post_privacy,
|
|
'public_key' => $this->compstr($this->_ssl_pubkey)
|
|
),
|
|
'comments' => array(
|
|
'past_month' => $rc->getCommentCount($last_month, true),
|
|
'past_year' => $rc->getCommentCount($last_year, true),
|
|
'total' => $rc->getCommentCount(Null, true)
|
|
),
|
|
'postdate_utc' => $this->local2utc($reftime_local),
|
|
);
|
|
|
|
// DEBUG Post
|
|
//print_r($message);
|
|
//die();
|
|
|
|
// Encode Message so it can be posted
|
|
$article = $this->encodePost(
|
|
$message, $reftime_local, false,
|
|
$prvkey, Null, true,
|
|
SpotNab::FETCH_DISCOVERY_TYPE,
|
|
SpotNab::AUTODISCOVER_POST_USER,
|
|
SpotNab::AUTODISCOVER_POST_EMAIL,
|
|
SpotNab::AUTODISCOVER_POST_GROUP
|
|
);
|
|
// DEBUG Post
|
|
//print_r($article);
|
|
//die();
|
|
|
|
if($article === false){
|
|
echo "Failed.\n";
|
|
return false;
|
|
}
|
|
|
|
// Post message
|
|
if ($this->_postArticle($article, $retries))
|
|
{
|
|
// Post is good; update database
|
|
//echo "Done.\n";
|
|
return true;
|
|
}
|
|
echo "Failed.\n";
|
|
return false;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function fetch($reftime=Null, $retries=3){
|
|
/*
|
|
* This function queries all enabled sources and fetches any content
|
|
* they are sharing.
|
|
|
|
* The specified $reftime is presumed to be local *not utc*
|
|
*/
|
|
|
|
$db = new Settings();
|
|
$last = $first = Null;
|
|
|
|
// Return Value; Initialize it to Okay
|
|
// we'll change it to false if we have to.
|
|
$fetch_okay = true;
|
|
$backfill = false;
|
|
|
|
// We set a cap on how many days in the past we look
|
|
$_max_age = time() - SpotNab::FETCH_MAXIMUM_AGE;
|
|
if($reftime === Null){
|
|
// Fetch local time (but look back X days)
|
|
$reftime = $this->utc2local($_max_age);
|
|
}else{
|
|
// Someone specified a date range to query from
|
|
$backfill = true;
|
|
|
|
if(is_string($reftime)){
|
|
$reftime = date("Y-m-d H:i:s", strtotime($reftime));
|
|
}else if(is_int($reftime)){
|
|
$reftime = date("Y-m-d H:i:s", $reftime);
|
|
}
|
|
$_max_age = strtotime($reftime);
|
|
}
|
|
|
|
// First we find all active sources and build a hash table we can
|
|
// use to simplify fetching.
|
|
$sql = "SELECT * FROM spotnabsources WHERE active = 1 ".
|
|
"ORDER BY usenetgroup,lastupdate DESC";
|
|
$res = $db->query($sql);
|
|
$group_hash = array();
|
|
$group_article_start = array();
|
|
$id_hash = array();
|
|
|
|
if(!count($res))
|
|
return true;
|
|
|
|
foreach($res as $source){
|
|
$ghash = trim($source['usenetgroup']);
|
|
if(!array_key_exists($ghash, $group_hash)){
|
|
// Because our results are sorted by group, if we enter
|
|
// here then we're processing a brand new group...
|
|
$group_hash[$ghash] = array();
|
|
|
|
// Initialize our article start point
|
|
$group_article_start[$ghash] = 0;
|
|
|
|
// Initialize id Hash
|
|
$id_hash[$ghash] = array();
|
|
}
|
|
|
|
// Reference time is in UTC on Usenet but local in our database
|
|
// this isn't intentionally confusing, this is done so all our local
|
|
// times reflect those across the world, and it also makes joins to
|
|
// the table much easier since they join doesn't have to accomodate
|
|
// for the utc time itself.
|
|
|
|
// Therefore, we need to take the lastupdate time and convert it to
|
|
// UTC for processing.
|
|
$ref = $backfill?date("Y-m-d H:i:s", $_max_age):$source['lastupdate'];
|
|
|
|
if(!$ref){
|
|
// We've never fetched from the group before, so we'll use
|
|
// the reftime passed to the function
|
|
$ref = $reftime;
|
|
}
|
|
|
|
// Therefore, we need to take the lastupdate time and convert it to
|
|
// UTC for processing.
|
|
$article = abs(intval($source['lastarticle']));
|
|
if($article > 0){
|
|
if($group_article_start[$ghash] == 0)
|
|
$group_article_start[$ghash] = $article;
|
|
else
|
|
$group_article_start[$ghash] = ($article < $group_article_start[$ghash])?
|
|
$article:$group_article_start[$ghash];
|
|
}
|
|
|
|
// Store id
|
|
$id_hash[$ghash][] = $source['id'];
|
|
|
|
// Store Source Details
|
|
$group_hash[$ghash][] = array(
|
|
'id' => $source['id'],
|
|
'key' => $this->decompstr(trim($source['publickey'])),
|
|
'user' => trim($source['username']),
|
|
'email' => trim($source['useremail']),
|
|
|
|
// Store local reference time
|
|
'ref' => strtotime($ref),
|
|
|
|
// Store last article reference
|
|
'article' => $article
|
|
);
|
|
}
|
|
|
|
// We want to resort the internal arrays by they're ref time
|
|
// so that the oldest (longest without an update) is processed
|
|
// first
|
|
foreach(array_keys($group_hash) as $key){
|
|
$_ref = array();
|
|
foreach($group_hash[$key] as $id => $source){
|
|
# Source Time (within reason)
|
|
if($backfill)
|
|
$_ref[$id] = $_max_age;
|
|
else
|
|
$_ref[$id] =
|
|
($source['ref'] < $_max_age)?$_max_age:$source['ref'];
|
|
}
|
|
// Sort results (oldest in time first)
|
|
array_multisort($_ref, SORT_ASC, $group_hash[$key]);
|
|
}
|
|
|
|
// Now we fetch headers
|
|
set_error_handler('snHandleError');
|
|
|
|
// Connect to server
|
|
try{
|
|
if (($this->_site->alternate_nntp == 1 ? $this->_nntp->doConnect(true, true) : $this->_nntp->doConnect()) !== true) {
|
|
exit($this->_pdo->log->error("Unable to connect to usenet." . PHP_EOL));
|
|
}
|
|
}
|
|
catch(Exception $e){
|
|
printf("Failed to connect to Usenet");
|
|
// Restore handler
|
|
restore_error_handler();
|
|
return false;
|
|
}
|
|
|
|
// Track how many records were inserted, updated
|
|
$inserted = 0;
|
|
$updated = 0;
|
|
|
|
foreach($group_hash as $group => $hash){
|
|
printf("Spotnab : %d source(s)...", count($hash));
|
|
|
|
$summary = $this->_nntp->selectGroup($group);
|
|
while(NNTP::isError($summary))
|
|
{
|
|
// Reset Connection (This happens first time
|
|
// around since _nntpReset() will perform
|
|
// the initial connect if it hasn't been already
|
|
$summary = $this->_nntpReset($group);
|
|
|
|
// Track retry attempts
|
|
$retries--;
|
|
if($retries <= 0){
|
|
// Retry Atempts exausted
|
|
printf("Spotnab : lost connection...");
|
|
// Restore handler
|
|
restore_error_handler();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Get our article id
|
|
$first = ($backfill)?0:$group_article_start[$group];
|
|
if($first == 0){
|
|
// We can safely use the first $hash entry since we've
|
|
// already sorted it in ascending order above, so this
|
|
// is the time furthest back
|
|
$first = $this->_first_article_by_date($group, $hash[0]['ref']);
|
|
if($first === false){
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Group Processing Initialization
|
|
$processed = 0;
|
|
$batch = $last = intval($summary['last']);
|
|
$total = abs($last-$first);
|
|
|
|
// Select Group
|
|
while($fetch_okay && $processed < $total)
|
|
{
|
|
try
|
|
{
|
|
// Prepare Initial Batch
|
|
if ($total > SpotNab::FETCH_USENET_BATCH)
|
|
$batch = $first + SpotNab::FETCH_USENET_BATCH;
|
|
|
|
// Batch Processing
|
|
while ($processed>=0 && $processed < $total)
|
|
{
|
|
$headers = $this->_get_headers($group,
|
|
"$first-$batch", $retries);
|
|
|
|
if($headers === false){
|
|
// Retry Atempts exausted
|
|
$fetch_okay = false;
|
|
break;
|
|
}
|
|
|
|
// Process the header batch
|
|
$saved = $this->process_comment_headers($headers, $hash);
|
|
if($saved !== false)
|
|
{
|
|
$inserted += $saved[0];
|
|
$updated += $saved[1];
|
|
}
|
|
|
|
$processed += ($batch-$first);
|
|
// Increment starting index
|
|
$first += ($batch-$first);
|
|
|
|
if ($last-$first >= SpotNab::FETCH_USENET_BATCH){
|
|
// Fetch next batch
|
|
$batch = $first + SpotNab::FETCH_USENET_BATCH;
|
|
}else{
|
|
$batch = $last;
|
|
}
|
|
//echo "$first-$batch, processed=$processed\n";
|
|
//print_r($headers);
|
|
}
|
|
|
|
}catch(Exception $e){
|
|
// Reset Connection
|
|
$fetch_okay = $this->_nntpReset($group);
|
|
|
|
// Track retry attempts
|
|
$retries--;
|
|
if($retries <= 0){
|
|
// Retry Atempts exausted
|
|
$fetch_okay = false;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
/*$sql = sprintf("UPDATE spotnabsources "
|
|
."SET lastarticle = %d WHERE id IN (%s)",
|
|
$last,
|
|
implode(",", $id_hash[$group]));*/
|
|
$db->queryExec(sprintf('UPDATE spotnabsources '
|
|
.'SET lastarticle = %d WHERE id IN (%s)',
|
|
$last,
|
|
implode(",", $id_hash[$group])));
|
|
echo "\n";
|
|
}
|
|
// Restore handler
|
|
restore_error_handler();
|
|
|
|
// Ensure We're not connected
|
|
try{$this->_nntp->doQuit();}
|
|
catch(Exception $e)
|
|
{/* do nothing */}
|
|
|
|
return $inserted + $updated;
|
|
}
|
|
|
|
public function processGID($limit=500, $batch=5000, $delete_broken_releases=false){
|
|
// Process until someone presses cntrl-c
|
|
|
|
$db = new Settings();
|
|
$nzb = new NZB();
|
|
|
|
$processed = 0;
|
|
|
|
// We need an offset for tracking unhandled issues
|
|
$offset = 0;
|
|
|
|
$fsql = 'SELECT id, name, guid FROM releases '
|
|
.'WHERE gid IS NULL ORDER BY adddate DESC LIMIT %d,%d';
|
|
$usql = "UPDATE releases SET gid = '%s' WHERE id = %d";
|
|
|
|
while(1){
|
|
// finish
|
|
if($limit > 0 && $processed >= $limit)
|
|
break;
|
|
$batch=($limit > 0 && $batch > $limit)?$limit:$batch;
|
|
$res = $db->query(sprintf($fsql, $offset, $batch));
|
|
if(!$res)break;
|
|
if(count($res) <= 0)break;
|
|
$offset += $batch;
|
|
|
|
foreach ($res as $r){
|
|
$nzbfile = $nzb->getNZBPath($r["guid"]);
|
|
if($nzbfile === Null){
|
|
continue;
|
|
}
|
|
|
|
$nzbInfo = new NZBInfo();
|
|
if (!$nzbInfo->loadFromFile($nzbfile))
|
|
{
|
|
if($delete_broken_releases){
|
|
$release = new Releases();
|
|
$release->deleteSingle(['g' => $r['guid'], 'i' => $r['id']], $this->nzb, $this->releaseImage);
|
|
// Free the variable in an attempt to recover memory
|
|
unset($release);
|
|
echo '-';
|
|
}else{
|
|
// Skip over this one for future fetches
|
|
$offset++;
|
|
}
|
|
continue;
|
|
}
|
|
$gid = false;
|
|
if (!empty($nzbInfo->gid))
|
|
$gid = $nzbInfo->gid;
|
|
// Free the variable in an attempt to recover memory
|
|
unset($nzbInfo);
|
|
|
|
if(!$gid){
|
|
if($delete_broken_releases){
|
|
$release = new Releases();
|
|
$release->$release->deleteSingle(['g' => $r['guid'], 'i' => $r['id']], $this->nzb, $this->releaseImage);
|
|
unset($release);
|
|
echo '-';
|
|
}else{
|
|
// Skip over this one for future fetches
|
|
$offset++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Update DB With Global Identifer
|
|
$ures = $db->queryExec(sprintf("UPDATE releases SET gid = %s WHERE id = %d", $db->escapeString($gid), $r['id']));
|
|
if($ures->rowCount() == 0){
|
|
printf("\nPostPrc : Failed to update: %s\n", $r['name']);
|
|
}
|
|
// make noise...
|
|
echo '.';
|
|
$processed += 1;
|
|
}
|
|
}
|
|
|
|
# Batch update for comment table
|
|
/*$usql = 'UPDATE releasecomment, releases '
|
|
.'SET releasecomment.gid = releases.gid, '
|
|
.'releasecomment.nzb_guid = releases.nzb_guid '
|
|
.'WHERE releases.id = releasecomment.releaseid '
|
|
.'AND releasecomment.gid IS NULL '
|
|
.'AND releasecomment.nzb_guid = "" '
|
|
.'AND releases.nzb_guid IS NOT NULL '
|
|
.'AND releases.gid IS NOT NULL ';*/
|
|
|
|
$affected = $db->queryExec(sprintf('UPDATE releasecomment, releases SET releasecomment.gid = releases.gid,
|
|
releasecomment.nzb_guid = releases.nzb_guid
|
|
WHERE releases.id = releasecomment.releaseid
|
|
AND releasecomment.gid IS NULL
|
|
AND releasecomment.nzb_guid = ""
|
|
AND releases.nzb_guid IS NOT NULL
|
|
AND releases.gid IS NOT NULL '
|
|
)
|
|
);
|
|
$rows = $affected->rowCount();
|
|
if($rows > 0)
|
|
$processed += $rows;
|
|
return $processed;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function keygen($print=true, $force_regen=false){
|
|
// Simply generate a Public/Private Key pair if they don't already
|
|
// exist
|
|
$db = new Settings();
|
|
|
|
// A small boolean we safely toggle after performing
|
|
// a few checks first to make sure it's safe to do so
|
|
$do_keygen = true;
|
|
|
|
if($force_regen === false){
|
|
// Not forcing, so we immediately toggle the keygen
|
|
// flag, we'll toggle it back if we feel the need
|
|
$do_keygen = false;
|
|
|
|
if($this->_ssl_pubkey && $this->_ssl_prvkey){
|
|
|
|
$str_in = $this->getRandomStr(80);
|
|
$str_out = $this->decrypt($this->encrypt($str_in));
|
|
|
|
if($str_in != $str_out){
|
|
// Our key isn't good for nothin...
|
|
// regen a new one
|
|
$do_keygen = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if($do_keygen)
|
|
{
|
|
// Set new Key
|
|
$keys = $this->_keygen();
|
|
if(is_array($keys)){
|
|
// Force New Username
|
|
$sql = sprintf("Update settings SET value = %s "
|
|
."WHERE setting = 'spotnabuser'",
|
|
$db->escapeString(sprintf("nntp-%s",substr(md5($keys['pubkey']), 0, 4))));
|
|
$db->queryExec($sql);
|
|
// Force New Email
|
|
$sql = sprintf("Update settings SET value = %s "
|
|
."WHERE setting = 'spotnabemail'",
|
|
$db->escapeString(sprintf("nntp-%s@%s.com",
|
|
substr(md5($keys['pubkey']), 4, 8),
|
|
substr(md5($keys['pubkey']), 8, 16)
|
|
)));
|
|
$db->queryExec($sql);
|
|
// Save Keys
|
|
$sql = sprintf("Update settings SET value = %s ".
|
|
"WHERE setting = 'spotnabsitepubkey'",
|
|
$db->escapeString($keys['pubkey']));
|
|
$db->queryExec($sql);
|
|
//echo $keys['pubkey']."\n";
|
|
|
|
$sql = sprintf("Update settings SET value = %s ".
|
|
"WHERE setting = 'spotnabsiteprvkey'",
|
|
$db->escapeString($keys['prvkey']));
|
|
$db->queryExec($sql);
|
|
|
|
// Update settings Information
|
|
$this->_globals = $this->_site;
|
|
$this->_post_user = trim($this->_globals->spotnabuser);
|
|
$this->_post_email = trim($this->_globals->spotnabemail);
|
|
$this->_ssl_pubkey = $this->decompstr($this->_globals->spotnabsitepubkey);
|
|
$this->_ssl_prvkey = $this->decompstr($this->_globals->spotnabsiteprvkey);
|
|
}else{
|
|
// echo "Failed.";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if($print){
|
|
printf("SPOTNAB USER : %s\n", $this->_post_user);
|
|
printf("SPOTNAB EMAIL : %s\n", $this->_post_email);
|
|
printf("SPOTNAB GROUP : %s\n", $this->_post_group);
|
|
printf("SPOTNAB PUBLIC KEY (Begin copy from next line):\n%s\n",
|
|
$this->_globals->spotnabsitepubkey);
|
|
}
|
|
|
|
return array(
|
|
'pubkey' => $this->_globals->spotnabsitepubkey,
|
|
'prvkey' => $this->_globals->spotnabsiteprvkey
|
|
);
|
|
}
|
|
|
|
// ***********************************************************************
|
|
protected function _first_article_by_date($group, $refdate, $limit=SpotNab::FETCH_MAXIMUM_HEADERS, $retries=3){
|
|
// fetches the first article starting at the time specified
|
|
// by ref time.
|
|
//
|
|
// ref time is expected to be a local time in format:
|
|
// YYYY-MM-DD hh:mm:ss or as integer
|
|
//
|
|
// This function returns the first message id to scan from
|
|
// based on the time specified. If no articles are found
|
|
// or something bad happens, false is returned.
|
|
|
|
$interval = 1;
|
|
|
|
// if we start charting into an area where retention period
|
|
// isn't present, we're dealing with a blank/dead record that
|
|
// is lost.... to many blanks and we have to abort.
|
|
$misses = 0;
|
|
|
|
// curfews are a way of not letting an infinit while
|
|
// loop from taking over while we hunt for a date...
|
|
// since usenet is not always listed chronologically
|
|
// we can get in endless loops trying to find the ealiest
|
|
// date. To handle that we set a curfew and drop back
|
|
// a set amount of records and work with that
|
|
// no one is perfect right?
|
|
$curfew = 10;
|
|
|
|
if(is_string($refdate)){
|
|
// Convert to Integer (Local Time)
|
|
$refdate = strtotime($refdate);
|
|
}
|
|
|
|
while(($retries > 0) && ($interval > 0)){
|
|
$summary = $this->_nntp->selectGroup($group);
|
|
if(NNTP::isError($summary))
|
|
{
|
|
// Reset Connection
|
|
$this->_nntpReset();
|
|
|
|
// Track retry attempts
|
|
$retries--;
|
|
if($retries <= 0){
|
|
// Retry Atempts exausted
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
$_last = $last = intval($summary['last']);
|
|
$_first = $first = intval($summary['first']);
|
|
|
|
$curdate = $lastdate = Null;
|
|
$curid = $lastid = $first;
|
|
$interval = 0;
|
|
while($retries > 0){
|
|
|
|
// Save Last Interval
|
|
$lastinterval = $interval;
|
|
|
|
// Adjust Interval
|
|
if(($last - $first) > 3)
|
|
$interval = floor(($last - $first)/2);
|
|
else
|
|
$interval = 1;
|
|
|
|
if($misses >= SpotNab::FETCH_MAX_MISSES){
|
|
// Misses reached
|
|
$last = intval($summary['last']);
|
|
// Adjust pointer
|
|
$lastid = ($lastid===false)?$first:$lastid;
|
|
if($lastid >0){
|
|
if (($last-$lastid) > $limit){
|
|
// We exceeded our maximum header limit
|
|
// adjust accordingly
|
|
$lastid = $last - $limit;
|
|
}
|
|
echo " ".(abs($last-$lastid))." record(s) back.";
|
|
return $lastid;
|
|
}else{
|
|
if (($_last-$last) > $limit){
|
|
// We exceeded our maximum header limit
|
|
// adjust accordingly
|
|
$last = $_last - $limit;
|
|
}
|
|
echo " ".(abs($_last-$last))." record(s) back.";
|
|
return $last;
|
|
}
|
|
}
|
|
|
|
// Swap
|
|
$lastdate = $curdate;
|
|
if($curid > 0)
|
|
$lastid = $curid;
|
|
|
|
$msgs = $this->_get_headers(
|
|
$group, ($last-$interval), $retries);
|
|
|
|
if($msgs === false){
|
|
if (($_last-$last) > $limit){
|
|
// We exceeded our maximum header limit
|
|
// adjust accordingly
|
|
$last = $_last - $limit;
|
|
}
|
|
echo " ".(abs($_last-$last))." record(s) back.";
|
|
return $last;
|
|
}
|
|
|
|
// Reset Miss Count
|
|
$misses = 0;
|
|
|
|
// Save Tracker
|
|
$curdate = strtotime($msgs[0]['Date']);
|
|
$curid = intval($msgs[0]['Number']);
|
|
if($curid <= 0){
|
|
$lastid = ($lastid===false)?$first:$lastid;
|
|
if($lastid >0){
|
|
if (($_last-$lastid) > $limit){
|
|
// We exceeded our maximum header limit
|
|
// adjust accordingly
|
|
$lastid = $_last - $limit;
|
|
}
|
|
echo " ".(abs($_last-$lastid))." record(s) back.";
|
|
return $lastid;
|
|
}else{
|
|
if (($_last-$last) > $limit){
|
|
// We exceeded our maximum header limit
|
|
// adjust accordingly
|
|
$last = $_last - $limit;
|
|
}
|
|
echo " ".(abs($_last-$last))." record(s) back.";
|
|
return $last;
|
|
}
|
|
}
|
|
|
|
//echo "\nCUR($curid):".date("Y-m-d H:i:s", $curdate)
|
|
// ." REF:".date("Y-m-d H:i:s", $refdate)
|
|
// ." LAST:".date("Y-m-d H:i:s", $lastdate)
|
|
// ." $interval\n";
|
|
|
|
if($interval == 1){
|
|
// We're soo close now...
|
|
$curfew --;
|
|
if($curfew <= 0)
|
|
{
|
|
if (($_last-$curid) > $limit){
|
|
// We exceeded our maximum header limit
|
|
// adjust accordingly
|
|
$curid = $_last - $limit;
|
|
}
|
|
// curfew met... just deal with our current spot
|
|
echo " ".($_last-$curid)." record(s) back.";
|
|
return $curid;
|
|
}
|
|
|
|
if($refdate > $curdate && $refdate > $lastdate){
|
|
if (($_last-$curid) > $limit){
|
|
// We exceeded our maximum header limit
|
|
// adjust accordingly
|
|
$curid = $_last - $limit;
|
|
}
|
|
// Found content
|
|
echo " ".($_last-$curid)." record(s) back.";
|
|
return $curid;
|
|
}else if($refdate > $curdate && $refdate > $lastdate){
|
|
// Close... Shuffle forward a bit
|
|
$first+=2;
|
|
}else{
|
|
// Slide window and try again
|
|
$last-=2;
|
|
}
|
|
$interval=2;
|
|
continue;
|
|
}
|
|
|
|
// Make some noise
|
|
if($interval%2)echo ".";
|
|
|
|
// Adjust Boundaries
|
|
if($curdate > $refdate){
|
|
// We need to look further forward
|
|
$last = $curid+1;
|
|
}else if ($curdate <= $refdate){
|
|
// We need To look further back
|
|
$first = $curid-1;
|
|
}
|
|
}
|
|
}
|
|
//echo "n/a m:$misses,i:$interval\n";
|
|
return false;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function process_comment_headers($headers, $group_hash, $save=true){
|
|
/*
|
|
* We iterate over the provided headers (generated by
|
|
* $this->_get_headers() to a structure that is at the very
|
|
* minimum looking like this:
|
|
*
|
|
* array (
|
|
* [0] => array (
|
|
* 'Number': <int>
|
|
* 'Subject': <string>
|
|
* 'From': <string>
|
|
* 'Date': <string>
|
|
* 'Message-ID': <string>
|
|
* 'Bytes': <int>
|
|
* 'Lines': <int>
|
|
* 'Epoch': <int>
|
|
* ),
|
|
* ...
|
|
* )
|
|
* From the structure above, we process our group hash and retrieve
|
|
* all the binary data we need on valid content.
|
|
*
|
|
* A group_hash() record looks like this:
|
|
* array(
|
|
* array(
|
|
* 'id': <int>,
|
|
* 'key': <string>,
|
|
* 'user': <string>,
|
|
* 'email': <string>,
|
|
* 'ref': <int>,
|
|
* ),
|
|
* array(
|
|
* 'id': <int>,
|
|
* 'key': <string>,
|
|
* 'user': <string>,
|
|
* 'email': <string>,
|
|
* 'ref': <int>,
|
|
* ),
|
|
* )
|
|
*/
|
|
|
|
if(!count($group_hash)){
|
|
// Nothing to process
|
|
return array();
|
|
}
|
|
|
|
//
|
|
// Prepare some general SQL Commands for saving later if all goes well
|
|
//
|
|
|
|
$db = new Settings();
|
|
$rc = new ReleaseComments();
|
|
|
|
// Comments
|
|
$sql_new_cmt = "INSERT INTO releasecomment (".
|
|
"id, sourceID, username, userid, gid, cid, isvisible, ".
|
|
"releaseid, `text`, createddate, issynced, nzb_guid) VALUES (".
|
|
"NULL, %d, %s, 0, %s, %s, %d, 0, %s, %s, 1, %s)";
|
|
$sql_upd_cmt = "UPDATE releasecomment SET ".
|
|
"isvisible = %d, `text` = %s".
|
|
"WHERE sourceID = %d AND gid = %s AND cid = %s AND nzb_guid = %s";
|
|
$sql_fnd_cmt = "SELECT count(id) as cnt FROM releasecomment ".
|
|
"WHERE sourceID = %d AND gid = %s AND cid = %s";
|
|
|
|
// Sync Times
|
|
$sql_sync = "UPDATE spotnabsources SET lastupdate = %s ".
|
|
"WHERE id = %d";
|
|
|
|
$matches = Null;
|
|
$processed = 0;
|
|
$updates = 0;
|
|
$inserts = 0;
|
|
foreach ($headers as $header){
|
|
// Preform some general scanning the header to determine
|
|
// if it could possibly be a valid post.
|
|
if(!preg_match(SpotNab::FETCH_MSGID_REGEX,
|
|
$header['Message-ID'], $matches)){
|
|
continue;
|
|
}
|
|
if($matches['domain'] != SpotNab::SEGID_DOMAIN)
|
|
continue;
|
|
|
|
if($matches['type'] != SpotNab::FETCH_COMMENT_TYPE)
|
|
continue;
|
|
|
|
// Now we check the subject line; it provides the first part of
|
|
// the key to determining if we should handle the message or not
|
|
if(!preg_match(SpotNab::FETCH_COMMENT_SUBJECT_REGEX,
|
|
$header['Subject'], $matches)){
|
|
continue;
|
|
}
|
|
|
|
// We have a match; So populate potential variables
|
|
$checksum = $matches['checksum'];
|
|
$refdate = $matches['utcref'];
|
|
$refdate_epoch = @strtotime($matches['utcref']. " UTC");
|
|
if($refdate_epoch === false || $refdate_epoch < 0){
|
|
// Bad time specified
|
|
continue;
|
|
}
|
|
// PreKey is used to attempt to run the decode algorithm
|
|
// a head of time.. if we can decrypt this we can probably
|
|
// assume the body will decode too (and won't be a waste of
|
|
// time to download it)
|
|
|
|
foreach($group_hash as $hash){
|
|
// Track how many records we handled
|
|
$processed++;
|
|
|
|
// First check the ref date... if it's newer then what we've
|
|
// already processed, then we'll just keep on chugging along.
|
|
if($refdate_epoch <= $hash['ref']){
|
|
continue;
|
|
}
|
|
|
|
// Scan header information for supported matches
|
|
if(!preg_match('/^(?P<user>[^<]+)<(?P<email>[^>]+)>$/',
|
|
$header['From'], $matches))
|
|
continue;
|
|
|
|
// Match against our sources posts
|
|
if(trim($matches['user']) != $hash['user'])
|
|
continue;
|
|
if(trim($matches['email']) != $hash['email'])
|
|
continue;
|
|
|
|
// If we reach here, we've found a header we can process
|
|
// The next step is to download the header's body
|
|
|
|
// We'll do some final verifications on it such as detect
|
|
// if the checksum is okay, and verify that the timestamp
|
|
// within the body matches that of the header... then we
|
|
// can start processing the guts of the body.
|
|
|
|
if($save){
|
|
// Download Body
|
|
$body = $this->_get_body($header['Group'],
|
|
$header['Message-ID']);
|
|
if($body === false){
|
|
continue;
|
|
}
|
|
//echo "DEBUG Close Match:\n";
|
|
//print_r($header);
|
|
|
|
// Decode Body
|
|
$body = $this->decodePost($body, $hash['key']);
|
|
if($body === false)
|
|
continue; // Decode failed
|
|
|
|
// Verify Body
|
|
if(!is_array($body))
|
|
continue; // not any array
|
|
|
|
if(!(bool)count(array_filter(array_keys($body), 'is_string')))
|
|
continue; // not an associative array
|
|
|
|
if((!array_key_exists('server', $body)) ||
|
|
(!array_key_exists('postdate_utc', $body)))
|
|
continue; // base structure missing
|
|
|
|
// Compare postdate_utc and ensure it matches header
|
|
// timestamp
|
|
if(preg_replace('/[^0-9]/', '',
|
|
$body['postdate_utc']) != $refdate)
|
|
continue;
|
|
|
|
// Comment Handling
|
|
if(array_key_exists('comments',$body) &&
|
|
is_array($body['comments'])){
|
|
$rc = new ReleaseComments();
|
|
|
|
foreach($body['comments'] as $comment){
|
|
|
|
// Verify Comment is parseable
|
|
if(!is_array($comment))
|
|
continue; // not an array
|
|
if(!count(array_filter(array_keys($comment))))
|
|
continue; // not an associative array
|
|
|
|
// Store isvisible flag
|
|
$is_visible = 1;
|
|
if(array_key_exists('is_visible', $comment))
|
|
$is_visible = (intval($comment['is_visible'])>0)?1:0;
|
|
|
|
// Check that comment doesn't already exist
|
|
$res = $db->queryOneRow(sprintf($sql_fnd_cmt,
|
|
$hash['id'],
|
|
$db->escapeString($comment['gid']),
|
|
$db->escapeString($comment['cid'])));
|
|
|
|
// Store Results in DB
|
|
if($res && intval($res['cnt'])>0){
|
|
// Make some noise
|
|
echo '.';
|
|
$updates += ($db->queryExec(sprintf($sql_upd_cmt,
|
|
$is_visible,
|
|
$db->escapeString($comment['comment']),
|
|
$hash['id'],
|
|
$db->escapeString($comment['gid']),
|
|
$db->escapeString($comment['cid']),
|
|
$db->escapeString($comment['gid'])
|
|
))>0)?1:0;
|
|
}else{
|
|
// Make some noise
|
|
echo '+';
|
|
// Perform Insert
|
|
$res = $db->queryInsert(sprintf($sql_new_cmt,
|
|
$hash['id'],
|
|
$db->escapeString($comment['username']),
|
|
$db->escapeString($comment['gid']),
|
|
$db->escapeString($comment['cid']),
|
|
$is_visible,
|
|
$db->escapeString($comment['comment']),
|
|
// Convert createddate to Local
|
|
$db->escapeString($this->utc2local(
|
|
$comment['postdate_utc'])),
|
|
$db->escapeString($comment['gid'])
|
|
));
|
|
$inserts += 1;
|
|
}
|
|
$rc->updateReleaseCommentCount($comment['gid']);
|
|
}
|
|
}
|
|
|
|
// Update spotnabsources table, set lastupdate to the
|
|
// timestamp parsed from the header.
|
|
$db->queryExec(sprintf($sql_sync,
|
|
$db->escapeString(
|
|
$this->utc2local($body['postdate_utc'])),
|
|
$hash['id']
|
|
)
|
|
);
|
|
}else{
|
|
// Debug non/save mode; mark update
|
|
$updates += 1;
|
|
}
|
|
|
|
// always break if we made it this far... no mater how many
|
|
// other groups are being processed, we've already matched
|
|
// for this article, so we don't need to process it for
|
|
// other sources.
|
|
break;
|
|
}
|
|
}
|
|
return array($inserts, $updates);
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function process_discovery_headers($headers, $save=true){
|
|
/*
|
|
* We iterate over the provided headers (generated by
|
|
* $this->_get_headers() to a structure that is at the very
|
|
* minimum looking like this:
|
|
*
|
|
* array (
|
|
* [0] => array (
|
|
* 'Number': <int>
|
|
* 'Subject': <string>
|
|
* 'From': <string>
|
|
* 'Date': <string>
|
|
* 'Message-ID': <string>
|
|
* 'Bytes': <int>
|
|
* 'Lines': <int>
|
|
* 'Epoch': <int>
|
|
* ),
|
|
* ...
|
|
* )
|
|
*/
|
|
|
|
//
|
|
// Prepare some general SQL Commands for saving later if all goes well
|
|
//
|
|
|
|
$db = new Settings();
|
|
|
|
// Auto Enable Flag (used for inserts only)
|
|
$auto_enable = ($this->_auto_enable)?"1":"0";
|
|
|
|
// Spotnab Sources
|
|
$sql_new_cmt = "INSERT INTO spotnabsources (".
|
|
"id, username, useremail, usenetgroup, publickey, ".
|
|
"active, description, lastupdate, lastbroadcast, dateadded) VALUES (".
|
|
"NULL, %s, %s, %s, %s, $auto_enable, %s, NULL, %s, NOW())";
|
|
$sql_upd_cmt = "UPDATE spotnabsources SET ".
|
|
"lastbroadcast = %s ".
|
|
"WHERE username = %s AND useremail = %s AND usenetgroup = %s";
|
|
$sql_fnd_cmt = "SELECT count(id) as cnt FROM spotnabsources ".
|
|
"WHERE username = %s AND useremail = %s AND usenetgroup = %s";
|
|
|
|
$matches = Null;
|
|
$processed = 0;
|
|
$inserts = 0;
|
|
$updates = 0;
|
|
foreach ($headers as $header){
|
|
// Preform some general scanning the header to determine
|
|
// if it could possibly be a valid post.
|
|
|
|
// Now we check the subject line; it provides the first part of
|
|
// the key to determining if we should handle the message or not
|
|
if(!preg_match(SpotNab::FETCH_MSGID_REGEX,
|
|
$header['Message-ID'], $matches)){
|
|
continue;
|
|
}
|
|
if($matches['domain'] != SpotNab::SEGID_DOMAIN)
|
|
continue;
|
|
|
|
if($matches['type'] != SpotNab::FETCH_DISCOVERY_TYPE)
|
|
continue;
|
|
|
|
// Now we check the subject line; it provides the first part of
|
|
// the key to determining if we should handle the message or not
|
|
if(!preg_match(SpotNab::FETCH_DISCOVERY_SUBJECT_REGEX,
|
|
$header['Subject'], $matches)){
|
|
continue;
|
|
}
|
|
|
|
// We have a match; So populate potential variables
|
|
$checksum = $matches['checksum'];
|
|
$refdate = $matches['utcref'];
|
|
$refdate_epoch = @strtotime($matches['utcref']. " UTC");
|
|
if($refdate_epoch === false || $refdate_epoch < 0){
|
|
// Bad time specified
|
|
continue;
|
|
}
|
|
// PreKey is used to attempt to run the decode algorithm
|
|
// a head of time.. if we can decrypt this we can probably
|
|
// assume the body will decode too (and won't be a waste of
|
|
// time to download it)
|
|
|
|
// Track how many records we handled
|
|
$processed++;
|
|
|
|
// Scan header information for supported matches
|
|
if(!preg_match('/^(?P<user>[^<]+)<(?P<email>[^>]+)>$/',
|
|
$header['From'], $matches))
|
|
continue;
|
|
|
|
// Match against our sources posts
|
|
if(trim($matches['user']) != SpotNab::AUTODISCOVER_POST_USER)
|
|
continue;
|
|
if(trim($matches['email']) != SpotNab::AUTODISCOVER_POST_EMAIL)
|
|
continue;
|
|
|
|
// If we reach here, we've found a header we can process
|
|
// The next step is to download the header's body
|
|
|
|
// We'll do some final verifications on it such as detect
|
|
// if the checksum is okay, and verify that the timestamp
|
|
// within the body matches that of the header... then we
|
|
// can start processing the guts of the body.
|
|
|
|
if($save){
|
|
// Download Body
|
|
$body = $this->_get_body($header['Group'],
|
|
$header['Message-ID']);
|
|
if($body === false){
|
|
continue;
|
|
}
|
|
//echo "DEBUG Close Match:\n";
|
|
//print_r($header);
|
|
|
|
// Decode Body
|
|
$body = $this->decodePost(
|
|
$body,
|
|
$this->decompstr($this->_ssl_auto_pubkey)
|
|
);
|
|
if($body === false)
|
|
continue; // Decode failed
|
|
|
|
//echo "DEBUG Close Match:\n";
|
|
//print_r($header);
|
|
|
|
// Verify Body
|
|
if(!is_array($body))
|
|
continue; // not any array
|
|
|
|
if(!(bool)count(array_filter(array_keys($body), 'is_string')))
|
|
continue; // not an associative array
|
|
|
|
if(!(array_key_exists('site', $body) &&
|
|
array_key_exists('posts', $body) &&
|
|
array_key_exists('comments', $body) &&
|
|
array_key_exists('postdate_utc', $body)))
|
|
continue; // base structure missing
|
|
|
|
// Compare postdate_utc and ensure it matches header
|
|
// timestamp
|
|
if(preg_replace('/[^0-9]/', '',
|
|
$body['postdate_utc']) != $refdate)
|
|
continue;
|
|
|
|
$posts = $body['posts'];
|
|
$p_user = array_key_exists('user', $posts)?trim($posts['user']):Null;
|
|
$p_email = array_key_exists('email', $posts)?trim($posts['email']):Null;
|
|
$p_group = array_key_exists('group', $posts)?trim($posts['group']):Null;
|
|
$p_key = array_key_exists('public_key', $posts)?trim($posts['public_key']):Null;
|
|
|
|
if(!($p_user && $p_email && $p_group && $p_key))
|
|
// basic error checking
|
|
continue;
|
|
|
|
// Check to make sure the discovery isn't 'this' site
|
|
if($p_user == $this->_post_user && $p_email == $this->_post_email)
|
|
continue;
|
|
|
|
// Check that comment doesn't already exist
|
|
$res = $db->queryOneRow(sprintf($sql_fnd_cmt,
|
|
$db->escapeString($p_user),
|
|
$db->escapeString($p_email),
|
|
$db->escapeString($p_group))
|
|
);
|
|
|
|
if(!$res)
|
|
// Uh oh
|
|
continue;
|
|
|
|
// Store Results in DB
|
|
if(intval($res['cnt'])==0){
|
|
// Make some noise
|
|
echo '+';
|
|
// Perform Insert
|
|
$res = $db->queryInsert(sprintf($sql_new_cmt,
|
|
$db->escapeString($p_user),
|
|
$db->escapeString($p_email),
|
|
$db->escapeString($p_group),
|
|
$db->escapeString($p_key),
|
|
// store user as description for now
|
|
$db->escapeString($p_user),
|
|
$db->escapeString($this->utc2local($refdate)))
|
|
);
|
|
$inserts += 1;
|
|
}else{
|
|
echo '.';
|
|
$res = $db->queryExec(sprintf($sql_upd_cmt,
|
|
$db->escapeString($this->utc2local($refdate)),
|
|
$db->escapeString($p_user),
|
|
$db->escapeString($p_email),
|
|
$db->escapeString($p_group)));
|
|
$updates += 1;
|
|
}
|
|
}
|
|
}
|
|
return array($inserts, $updates);
|
|
}
|
|
|
|
// ***********************************************************************
|
|
protected function _get_body($group, $id, $retries=3){
|
|
/*
|
|
* Fetch the body of a given Message-ID taken from the headers
|
|
* The function then returns the raw content
|
|
*/
|
|
|
|
$matches = Null;
|
|
if(preg_match("/^\s*<(.*)>\s*$/", $id, $matches))
|
|
// Ensure we're always dealing with a properly
|
|
// formatted id
|
|
$id = $matches[1];
|
|
|
|
// The returned result will be stored in $raw
|
|
$raw = Null;
|
|
do
|
|
{
|
|
$raw = $this->_nntp->getBody("<".$id.">", true);
|
|
if(NNTP::isError($raw))
|
|
{
|
|
// Reset Connection (This happens first time
|
|
// around since _nntpReset() will perform
|
|
// the initial connect if it hasn't been already
|
|
$this->_nntpReset($group);
|
|
// Track retry attempts
|
|
$retries--;
|
|
if($retries <= 0){
|
|
// Retry Atempts exausted
|
|
printf("%s ERROR - Failed Segment Fetch: %s/%s ...\n",
|
|
date("Y-m-d H:i:s"), $group, $id);
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Retrieved Data
|
|
return $raw;
|
|
}while($retries > 0);
|
|
|
|
// Fail
|
|
return false;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
protected function _get_headers($group, $range, $retries=3, $sort=true){
|
|
/*
|
|
*
|
|
* There is to much involved with fetching article headers
|
|
* that bloat and make a lot of code repetative...
|
|
* This function returns the headers of the specified range
|
|
* in an array() of associative array() always to make life
|
|
* easy... alternativly, if this function fails then false
|
|
* is returned.
|
|
*
|
|
* We also convert all time scanned into its Epoch value
|
|
* with the returned results for easier parsing; this
|
|
* is done to order results as well.
|
|
*/
|
|
|
|
// epoch array is used for sorting fetched results
|
|
$epoch = array();
|
|
|
|
// Header parsing for associative array returned
|
|
$min_headers = array('Number', 'Subject', 'From', 'Date',
|
|
'Message-ID', 'Bytes', 'Lines');
|
|
do
|
|
{
|
|
$msgs = $this->_nntp->getOverview($range, true, false);
|
|
if(NNTP::isError($msgs))
|
|
{
|
|
$this->_nntpReset($group);
|
|
// Track retry attempts
|
|
$retries--;
|
|
if($retries <= 0){
|
|
// Retry Atempts exausted
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
// If we get here, then we fetched the header block okay
|
|
|
|
// Clean up bad results but don't mark fetch as a failure
|
|
// just report what it found.. (nothing). We do this because
|
|
// NNTP::isError() never threw, so the response has to be valid
|
|
// even though it's inconsistent
|
|
if(!$msgs)return array();
|
|
if(!is_array($msgs))return array();
|
|
|
|
// For whatever reason, we sometimes get an array of
|
|
// associative array returned, and all other times we just
|
|
// get an associative array. Convert the associative array
|
|
// if we get one to an array of associative array just to
|
|
// simplify the response and make it esier to work with
|
|
if((bool)count(array_filter(array_keys($msgs), 'is_string'))){
|
|
// convert to an array of assocative array
|
|
$msgs = array($msgs);
|
|
}
|
|
|
|
for($i=0;$i<count($msgs);$i++){
|
|
$skip=false;
|
|
foreach($min_headers as $key){
|
|
if(!array_key_exists($key, $msgs[$i])){
|
|
unset($msgs[$i]);
|
|
$i--;
|
|
$skip=true;
|
|
break;
|
|
}
|
|
}
|
|
if($skip)continue;
|
|
|
|
// Update Record With Epoch Value (# of sec from Jan, 1980)
|
|
$epoch[$i] = $msgs[$i]['Epoch'] = strtotime($msgs[$i]['Date']);
|
|
// It's easier to track the group information if it's
|
|
// stored with the header segment
|
|
$epoch[$i] = $msgs[$i]['Group'] = $group;
|
|
}
|
|
|
|
if($sort && count($msgs)>1)
|
|
// Content is already sorted by articles, but if the
|
|
// sort flag is specified, then content is re-sorted by the
|
|
// messages stored epoch time
|
|
array_multisort($epoch, SORT_ASC, $msgs);
|
|
|
|
return $msgs;
|
|
|
|
}while($retries >0);
|
|
|
|
return false;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function post($reftime=Null, $retries=3){
|
|
/*
|
|
* This function posts to usenet if there are any new updates
|
|
* to report that are flagged for transmit.
|
|
*
|
|
* The specified $reftime is presumed to be local *not utc*
|
|
*/
|
|
|
|
// Make sure we can post
|
|
if(!$this->_can_post){
|
|
// Disabled
|
|
return false;
|
|
}
|
|
|
|
$reftime_local = $reftime;
|
|
$article = Null;
|
|
if($reftime_local === Null){
|
|
// Fetch local time
|
|
$reftime_local = $this->utc2local();
|
|
}
|
|
// Header
|
|
$message = array(
|
|
'server' => array(
|
|
'code' => $this->_post_site,
|
|
'title' => $this->_post_title,
|
|
),
|
|
'postdate_utc' => $this->local2utc($reftime_local),
|
|
'comments' => array()
|
|
);
|
|
|
|
// Store Comments
|
|
while(($data = $this->unPostedComments()) !== Null)
|
|
{
|
|
$message['comments'] = $data['comments'];
|
|
|
|
$db = new Settings();
|
|
$sql = sprintf("UPDATE releasecomment "
|
|
."SET issynced = 1 WHERE id IN (%s)",
|
|
implode(",", $data['ids']));
|
|
|
|
// Generate keys if one doesn't exist
|
|
if(!($this->_ssl_prvkey && $this->_ssl_pubkey))
|
|
{
|
|
if($this->keygen(false, true) !== false)
|
|
// Post a discovery message if enabled
|
|
$this->post_discovery();
|
|
else
|
|
return false;
|
|
}
|
|
|
|
//printf("%s INFO - Encoding article ...", date("Y-m-d H:i:s"));
|
|
// Encode Message so it can be posted
|
|
$article = $this->encodePost($message, $reftime_local);
|
|
if($article === false){
|
|
echo "Failed.\n";
|
|
return false;
|
|
}
|
|
//echo "Done.\n";
|
|
|
|
// Post message
|
|
printf("Spotnab : %d posting ...\n", count($message['comments']));
|
|
if (!$this->_postArticle($article, $retries))
|
|
{
|
|
// Post is good; update database
|
|
$res = $db->queryExec($sql);
|
|
echo "Failed.\n";
|
|
return false;
|
|
}
|
|
//echo "Done.\n";
|
|
|
|
// Update Database
|
|
$res = $db->queryExec($sql);
|
|
}
|
|
|
|
// If code reached here then we're good
|
|
return true;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
private function _postArticle ($article, $retries=3)
|
|
{
|
|
// Extract message id
|
|
if(!preg_match('/Message-ID: <(?P<id>[^>]+)>/', $article[0], $matches)){
|
|
// we couldn't extract the message id
|
|
return false;
|
|
}
|
|
|
|
$msg_id = $matches['id'];
|
|
|
|
set_error_handler('snHandleError');
|
|
|
|
// Connect to server
|
|
if (($this->_site->alternate_nntp == 1 ? $this->_nntp->doConnect(true, true) : $this->_nntp->doConnect()) !== true) {
|
|
exit($this->_pdo->log->error("Unable to connect to usenet." . PHP_EOL));
|
|
}
|
|
while($retries > 0)
|
|
{
|
|
try
|
|
{
|
|
$summary = $this->_nntp->selectGroup($this->_post_group);
|
|
if(NNTP::isError($summary)){
|
|
$summary = $this->_nntpReset($this->_post_group);
|
|
$retries--;
|
|
continue;
|
|
}
|
|
// Check if server will receive an article
|
|
$_err = $this->_nntp->cmdPost();
|
|
if (NNTP::isError($_err)) {
|
|
$summary = $this->_nntpReset($this->_post_group);
|
|
$retries--;
|
|
continue;
|
|
}
|
|
|
|
// Actually send the article
|
|
$_err = $this->_nntp->cmdPost2($article);
|
|
|
|
}catch(Exception $e){
|
|
// Ensure We're not connected
|
|
try{$this->_nntp->doQuit();}
|
|
catch(Exception $e)
|
|
{/* do nothing */}
|
|
|
|
// Post failed
|
|
$retries--;
|
|
// try again
|
|
continue;
|
|
}
|
|
|
|
// Now we verify the post worked okay
|
|
// The below code was commented out but not removed as it
|
|
// is good reference on how to quickly scan for an article.
|
|
// The problem with the below code is some providers were
|
|
// taking up to 20 min for the post to show... so checking
|
|
// right after posting was failing for this group.
|
|
|
|
// $summary = $this->_nntp->selectGroup($this->_post_group);
|
|
// if(NNTP::isError($summary)) {
|
|
// $summary = $this->_nntpReset($this->_post_group);
|
|
// $retries--;
|
|
// continue;
|
|
// }
|
|
// $last = intval($summary['last']);
|
|
// $batch = $last - SpotNab::VERIFY_FETCH_HEADER_COUNT;
|
|
// $headers = $this->_get_headers($this->_post_group,
|
|
// "$batch-$last", $retries);
|
|
|
|
// // Ensure We're not connected
|
|
// try{$this->_nntp->doQuit();}
|
|
// catch(Exception $e)
|
|
// {/* do nothing */}
|
|
|
|
// if($headers === false){
|
|
// // We failed
|
|
// return false;
|
|
// }
|
|
|
|
// // Okay we have headers to scan (we'll work back from the end to
|
|
// // the front to speed the process up)
|
|
// for($i=count($headers)-1; $i >0; $i--){
|
|
// if(!array_key_exists('Message-ID', $headers[$i]))
|
|
// continue;
|
|
|
|
// if ($headers[$i]['Message-ID'] == "<$msg_id>")
|
|
// // Great! we found the post we just posted
|
|
// return true;
|
|
// }
|
|
// // Otherwise we didn't find the Message-ID so we fail
|
|
// return false;
|
|
|
|
// Restore handler
|
|
restore_error_handler();
|
|
|
|
// We're done
|
|
return true;
|
|
}
|
|
// Restore handler
|
|
restore_error_handler();
|
|
return false;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
private function _nntpReset ($group=Null)
|
|
{
|
|
// Reset Connection
|
|
try{$this->_nntp->doQuit();}
|
|
catch(Exception $e)
|
|
{/* do nothing */}
|
|
|
|
// Attempt to reconnect
|
|
try{
|
|
if (($this->_site->alternate_nntp == 1 ? $this->_nntp->doConnect(true, true) : $this->_nntp->doConnect()) !== true) {
|
|
exit($this->_pdo->log->error("Unable to connect to usenet." . PHP_EOL));
|
|
}
|
|
}
|
|
catch(Exception $e){return false;}
|
|
|
|
if($group !== Null)
|
|
{
|
|
// Reselect group if specified
|
|
$summary = $this->_nntp->selectGroup($this->_post_group);
|
|
if(NNTP::isError($summary))
|
|
return false;
|
|
return $summary;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function getRandomStr($len) {
|
|
// Valid Characters
|
|
static $vc = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
|
|
|
|
$unique = '';
|
|
for($i = 0; $i < $len; $i++)
|
|
$unique .= $vc[mt_rand(0, strlen($vc) - 1)];
|
|
|
|
return $unique;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function decodePost($message, $key=Null, $decrypt=true) {
|
|
|
|
// Decode Yenc
|
|
$message = $this->_nntp->decodeYenc2($message);
|
|
|
|
// Decompress Messsage
|
|
$message = @gzuncompress($message);
|
|
|
|
if ($key === Null)
|
|
$key = $this->_ssl_pubkey;
|
|
|
|
// Decrypt Message
|
|
if($decrypt){
|
|
$message = $this->decrypt($message, $key);
|
|
if($message === false){
|
|
// fail
|
|
return false;
|
|
}
|
|
}else{
|
|
// Convert from base64
|
|
$message = base64_decode($message);
|
|
if($message === false){
|
|
// Fail
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$message = json_decode($message, true);
|
|
if($message === false){
|
|
// Fail
|
|
return false;
|
|
}
|
|
|
|
return $message;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function encodePost($message, $reftime=Null, $debug=False,
|
|
$prvkey=Null, $passphrase=Null, $encrypt=True,
|
|
$msgtype=SpotNab::FETCH_COMMENT_TYPE,
|
|
$user=Null, $email=Null, $group=Null) {
|
|
/*
|
|
|
|
Assembles and encodes a message ready to be posted onto
|
|
a usenet server.
|
|
|
|
false is returned if the function fails,
|
|
|
|
If a reftime is specified, it is presumed that it will be in
|
|
an integer format and it will be localtime
|
|
|
|
If the debug is set to true, then a third part of the
|
|
article is returned containing header information that would
|
|
look as though _get_header() returned it
|
|
*/
|
|
|
|
// Assumed to be in Y-m-d H:i:s format or int
|
|
// convert local time into UTC
|
|
$reftime = $this->local2utc($reftime, "YmdHis");
|
|
$s = new Sites();
|
|
|
|
$msgid = sprintf('<%s.%s.%d@%s>',
|
|
$this->getRandomStr(30),
|
|
$msgtype,
|
|
time(),
|
|
SpotNab::SEGID_DOMAIN
|
|
);
|
|
|
|
if(!is_string($message)){
|
|
// If message is not already in string format, then
|
|
// it's in it's assembled mixed array format... we
|
|
// need to convert it to json before proceeding
|
|
$message = json_encode($message, JSON_HEX_TAG|JSON_HEX_APOS|
|
|
JSON_HEX_QUOT|JSON_HEX_AMP|JSON_UNESCAPED_UNICODE);
|
|
if($message === false){
|
|
// Fail
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// nntp posting expects an array as follows:
|
|
// array(
|
|
// [0] => 'Message Header'
|
|
// [1] => 'Message Body'
|
|
// );
|
|
|
|
if($encrypt){
|
|
// Encrypt Message
|
|
$message = $this->encrypt($message, $prvkey, $passphrase);
|
|
if($message === false){
|
|
// fail
|
|
return false;
|
|
}
|
|
}else{
|
|
// Convert to base64
|
|
$message = base64_encode($message);
|
|
}
|
|
|
|
// Compress Messsage
|
|
$message = @gzcompress($message, 9);
|
|
|
|
// Yenc Binary Content
|
|
$message = $this->_nntp->encodeYenc2($message, md5($message));
|
|
|
|
//
|
|
// Prepare Header
|
|
//
|
|
|
|
// Checksum id
|
|
$checksum = sha1($message);
|
|
|
|
// Prepare Subject
|
|
$subject = sprintf("%s-%s",
|
|
// checksum against message transmitted
|
|
$checksum,
|
|
// Save UTC Time
|
|
$reftime
|
|
);
|
|
|
|
if($user === Null)
|
|
$user = trim($this->_post_user);
|
|
if($email === Null)
|
|
$email = trim($this->_post_email);
|
|
if($group === Null)
|
|
$group = trim($this->_post_group);
|
|
|
|
$header = "Subject: " . $subject . "\r\n";
|
|
$header .= "Newsgroups: " . $group . "\r\n";
|
|
$header .= "Message-ID: $msgid\r\n";
|
|
$header .= "X-Newsreader: NewzNab v" . $s->version() ."\r\n";
|
|
$header .= "X-No-Archive: yes\r\n";
|
|
|
|
$header .= "From: ".$user. " <" . $email . ">\r\n";
|
|
|
|
// Binary Content
|
|
$header .= 'Content-Type: text/plain; charset=ISO-8859-1' . "\r\n";
|
|
$header .= 'Content-Transfer-Encoding: 8bit' . "\r\n";
|
|
|
|
// Assemble Article in structure NNTP expects
|
|
$article = array($header, $message);
|
|
|
|
if($debug){
|
|
// Append some debug data to the article
|
|
$article[] = array(
|
|
'Number' => 1234,
|
|
'Subject' => $subject,
|
|
'From' => sprintf('%s <%s>', $user, $email),
|
|
'Date' => date(DATE_RFC822, strtotime(
|
|
$this->utc2local($reftime))),
|
|
'Message-ID' => $msgid,
|
|
'Bytes' => strlen($message),
|
|
'Lines' => '1',
|
|
'Epoch' => strtotime($this->utc2local($reftime)),
|
|
'Group' => $group
|
|
);
|
|
}
|
|
return $article;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function unPostedComments($limit=SpotNab::POST_MAXIMUM_COMMENTS) {
|
|
/*
|
|
* This function returns a list of comments that have not been
|
|
* otherwise posted to usenet.
|
|
*
|
|
* $from and $to will configure themselves if set to NULL
|
|
* but otherwise it's expected format is string "Y-m-d H:i:s"
|
|
*/
|
|
|
|
$db = new Settings();
|
|
|
|
// Now we fetch for any new posts since reference point
|
|
$sql = sprintf("SELECT r.gid, rc.id, rc.text, u.username, "
|
|
."rc.isvisible, rc.createddate, rc.host "
|
|
."FROM releasecomment rc "
|
|
."JOIN releases r ON r.id = rc.releaseid AND rc.releaseid != 0 "
|
|
."JOIN users u ON rc.userid = u.id AND rc.userid != 0 "
|
|
."WHERE r.gid IS NOT NULL "
|
|
."AND sourceID = 0 AND issynced = 0 "
|
|
."LIMIT %d", $limit);
|
|
|
|
$res = $db->query($sql);
|
|
if(!$res)
|
|
return Null;
|
|
|
|
// Now we prepare a comments array to return with
|
|
$comments = array();
|
|
$ids = array();
|
|
|
|
foreach($res as $comment){
|
|
// If we don't have a gid then we can't make the post;
|
|
// the user hasn't set up there database to store the gid's
|
|
// correctly
|
|
|
|
if(empty($comment['gid']))
|
|
continue;
|
|
|
|
// Privacy options (scramble username or not)
|
|
if ($this->_post_privacy)
|
|
$username = sprintf(
|
|
"sn-%s",
|
|
substr(md5($comment['username'].$this->_globals->siteseed), 0, 6)
|
|
);
|
|
else
|
|
$username = $comment['username'];
|
|
|
|
// Hash a unique Comment id to associate with this message
|
|
$cid = md5($comment['id'].$comment['username'].$comment['createddate'].$comment['host']);
|
|
|
|
// Keep list of IDs (required for cleanup)
|
|
$ids[] = $comment['id'];
|
|
|
|
// Build Comment
|
|
$comments[] = array(
|
|
// Release Global id
|
|
'gid' => $comment['gid'],
|
|
// Comment id
|
|
'cid' => $cid,
|
|
// Store comment
|
|
'comment' => $comment['text'],
|
|
// Store comment
|
|
'username' => $username,
|
|
// Store visibility flag
|
|
'is_visible' => $comment['isvisible'],
|
|
// Convert createddate to UTC
|
|
'postdate_utc' => $this->local2utc($comment['createddate'])
|
|
);
|
|
}
|
|
|
|
// Return Results if they are present
|
|
return (count($comments)>0)?
|
|
array('comments' => $comments, 'ids' => $ids):Null;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function utc2local($utc=Null, $format="Y-m-d H:i:s") {
|
|
/*
|
|
* Takes a utc time as input and outputs local
|
|
* If no argument is specified then current local
|
|
* time is returned.
|
|
*/
|
|
if(is_string($utc)){
|
|
return date($format, strtotime($utc. " UTC"));
|
|
}else if(is_int($utc)){
|
|
return date($format, strtotime(date($format, $utc)." UTC"));
|
|
}
|
|
return date($format);
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function local2utc($local=Null, $format="Y-m-d H:i:s") {
|
|
/*
|
|
* Takes a local time as input and outputs UTC
|
|
* If no argument is specified then current UTC
|
|
* time is returned.
|
|
*/
|
|
if(is_string($local)){
|
|
return gmdate($format, strtotime($local));
|
|
}else if(is_int($local)){
|
|
return gmdate($format, $local);
|
|
}
|
|
return gmdate($format);
|
|
}
|
|
|
|
// ***********************************************************************
|
|
private function _keygen($passphrase=Null, $bits=1024,
|
|
$type=OPENSSL_KEYTYPE_RSA)
|
|
{
|
|
if(!function_exists('openssl_pkey_new'))
|
|
return false;
|
|
|
|
//Generate Key
|
|
$res = openssl_pkey_new(
|
|
array(
|
|
'private_key_bits' => $bits,
|
|
'private_key_type' => $type,
|
|
'config' => OPENSSL_CFG_PATH
|
|
)
|
|
);
|
|
|
|
if ($res === false)
|
|
{
|
|
//print_r(openssl_error_string() );
|
|
return false;
|
|
}
|
|
|
|
// Get Private Key
|
|
openssl_pkey_export($res, $prvkey, $passphrase,
|
|
array('config' => OPENSSL_CFG_PATH));
|
|
|
|
// Get Public Key
|
|
$details = openssl_pkey_get_details($res);
|
|
if($details === false){
|
|
return false;
|
|
}
|
|
$pubkey = $details['key'];
|
|
|
|
return array(
|
|
'pubkey' => $this->compstr($pubkey),
|
|
'prvkey' => $this->compstr($prvkey),
|
|
);
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function encrypt ($source, $prvkey=Null, $passphrase=Null){
|
|
// Encryption performed using private key
|
|
if($prvkey === Null)
|
|
// Default Key if none is specified
|
|
$prvkey = $this->_ssl_prvkey;
|
|
|
|
if(!$prvkey)
|
|
// Still no key...
|
|
return false;
|
|
|
|
if(!function_exists('openssl_get_privatekey'))
|
|
return false;
|
|
|
|
// Load Public Key into array
|
|
$crypttext='';
|
|
$pkey = openssl_get_privatekey($prvkey, $passphrase);
|
|
if($pkey === false)
|
|
// bad key
|
|
return false;
|
|
|
|
$batch = $len = strlen($source);
|
|
$ptr = 0;
|
|
$encrypted = '';
|
|
|
|
while($len > 0){
|
|
// Prepare batch size
|
|
$batch = (($len - SpotNab::SSL_MAX_BUF_LEN) > 0)?SpotNab::SSL_MAX_BUF_LEN:$len;
|
|
|
|
$res = openssl_private_encrypt(substr($source, $ptr, $batch), $crypttext, $pkey);
|
|
if($res === false){
|
|
// Encryption failed
|
|
openssl_free_key($pkey);
|
|
return false;
|
|
}
|
|
$encrypted .= base64_encode($crypttext) . SpotNab::SSL_BUF_DELIMITER;
|
|
$len -= $batch;
|
|
$ptr += $batch;
|
|
}
|
|
openssl_free_key($pkey);
|
|
return $encrypted;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function decrypt ($source, $pubkey=Null){
|
|
// Decryption performed using public key
|
|
if($pubkey === Null)
|
|
// Default Key if none is specified
|
|
$pubkey = $this->_ssl_pubkey;
|
|
|
|
if(!$pubkey)
|
|
// Still no key...
|
|
return false;
|
|
|
|
if(!function_exists('openssl_get_publickey'))
|
|
return false;
|
|
|
|
$pkey = openssl_get_publickey($pubkey);
|
|
if($pkey === false){
|
|
// bad key
|
|
//echo openssl_error_string();
|
|
return false;
|
|
}
|
|
|
|
$cryptlist = explode(SpotNab::SSL_BUF_DELIMITER, $source);
|
|
|
|
$decrypted = '';
|
|
foreach($cryptlist as $crypt){
|
|
if(!strlen($crypt))break;
|
|
$_crypt = base64_decode($crypt);
|
|
if($_crypt === false){
|
|
// Fail
|
|
return false;
|
|
}
|
|
|
|
$res = openssl_public_decrypt($_crypt, $out, $pkey, OPENSSL_PKCS1_PADDING);
|
|
if($res === false){
|
|
// Decryption failed
|
|
//echo "DEBUG: ".openssl_error_string()."\n";
|
|
openssl_free_key($pkey);
|
|
return false;
|
|
}
|
|
$decrypted .= $out;
|
|
}
|
|
openssl_free_key($pkey);
|
|
|
|
return $decrypted;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function compstr ($str){
|
|
/*
|
|
* Compress a string
|
|
*/
|
|
$str = @gzcompress($str);
|
|
return base64_encode($str);
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function decompstr ($str){
|
|
/*
|
|
* De-compress a string
|
|
*/
|
|
$str = base64_decode($str);
|
|
return @gzuncompress($str);
|
|
}
|
|
|
|
public function getSources()
|
|
{
|
|
$db = new Settings();
|
|
return $db->query("SELECT id, lastupdate,lastbroadcast, active, description, "
|
|
."(SELECT count(id) from releasecomment where sourceid = s.id)"
|
|
." AS comments FROM spotnabsources s");
|
|
}
|
|
|
|
public function getSourceById($id)
|
|
{
|
|
$db = new Settings();
|
|
$sql = sprintf("SELECT * FROM spotnabsources WHERE id = %d", $id);
|
|
return $db->queryOneRow($sql);
|
|
}
|
|
|
|
public function addSource($description,$username,$usermail,$usenetgroup,$publickey)
|
|
{
|
|
$db = new Settings();
|
|
$sql = sprintf("INSERT INTO spotnabsources "
|
|
."(description, username, useremail,"
|
|
." usenetgroup, publickey, active) "
|
|
."VALUES (%s, %s, %s, %s, %s, 0)",
|
|
$db->escapeString($description), $db->escapeString($username),
|
|
$db->escapeString($usermail), $db->escapeString($usenetgroup),
|
|
$db->escapeString($publickey));
|
|
return $db->queryInsert($sql);
|
|
}
|
|
|
|
public function updateSource($id, $description,$username,$usermail,$usenetgroup,$publickey)
|
|
{
|
|
$db = new Settings();
|
|
return $db->queryExec(
|
|
sprintf("UPDATE spotnabsources SET "
|
|
."description = %s, username = %s, useremail = %s,"
|
|
." usenetgroup = %s, publickey = %s WHERE id= %d",
|
|
$db->escapeString($description), $db->escapeString($username),
|
|
$db->escapeString($usermail), $db->escapeString($usenetgroup),
|
|
$db->escapeString($publickey), $id));
|
|
}
|
|
|
|
public function deleteSource($id)
|
|
{
|
|
$db = new Settings();
|
|
return $db->queryExec(sprintf("DELETE FROM spotnabsources WHERE id = %d", $id));
|
|
}
|
|
|
|
public function toggleSource($id, $active)
|
|
{
|
|
$db = new Settings();
|
|
return $db->queryExec(sprintf("update spotnabsources SET active = %d WHERE id = %d", $active, $id));
|
|
}
|
|
|
|
public function getDefaultValue($table,$field)
|
|
{
|
|
$db = new Settings();
|
|
return $db->query(sprintf("SHOW COLUMNS FROM %s WHERE field = %s", $table, $db->escapeString($field)));
|
|
}
|
|
} |