[0-9a-z]{40})-(?P[0-9]{14})$/i'; // Discovery types are always 01 const FETCH_DISCOVERY_TYPE = "01"; const FETCH_DISCOVERY_SUBJECT_REGEX = '/^(?P[0-9a-z]{40})-(?P[0-9]{14})$/i'; // The Message id can be parsed as follows const FETCH_MSGID_REGEX = '/^<(?P[a-z0-9]{30})\.(?P[0-9]{2})\.(?P[0-9]+)[^@]*@(?P.*)>$/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); $this->_post_user = $post_user; $this->_post_email = $post_email; $this->_post_group = $post_group; // Fetch Meta information $this->_post_code = trim($this->_pdo->getSetting('code'))? $this->_pdo->getSetting('code'):Null; $this->_post_title = trim($this->_pdo->getSetting('title'))? $this->_pdo->getSetting('title'):Null; if ($this->_post_user === Null){ // Fetch the SpotNab UserID $this->_post_user = trim($this->_pdo->getSetting('spotnabuser'))? $this->_pdo->getSetting('spotnabuser'):Null; } if ($this->_post_email === Null){ // Fetch the SpotNab EmailID $this->_post_email = trim($this->_pdo->getSetting('spotnabemail'))? $this->_pdo->getSetting('spotnabemail'):Null; } if ($this->_post_group === Null){ // Fetch the SpotNab Usenet Group $this->_post_group = trim($this->_pdo->getSetting('spotnabgroup'))? $this->_pdo->getSetting('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('spotnabsiteprvkey'))? $this->_pdo->getSetting('spotnabsiteprvkey'):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('spotnabprivacy')) == 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->_pdo->getSetting('spotnabbroadcast')) == 1)? true:false; $this->_can_broadcast = ($this->_can_broadcast && $this->_can_post); $this->_can_discover = (trim($this->_pdo->getSetting('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 = []; $gids_matched = []; 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->_pdo->getSetting('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 = [ 'site' => [ // title & code taken out to keep things anonymous for now //'title' => $this->_post_title, //'code' => $this->_post_code, 'id' => md5($this->_pdo->getSetting('siteseed')), 'users' => $us->getCount() ], 'posts' => [ 'user' => $this->_post_user, 'email' => $this->_post_email, 'group' => $this->_post_group, 'privacy' => $this->_post_privacy, 'public_key' => $this->compstr($this->_ssl_pubkey) ], 'comments' => [ '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 = []; $group_article_start = []; $id_hash = []; 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] = []; // Initialize our article start point $group_article_start[$ghash] = 0; // Initialize id Hash $id_hash[$ghash] = []; } // 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][] = [ '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 = []; 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->_pdo->getSetting('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); // 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->_post_user = trim($this->_pdo->getSetting('spotnabuser')); $this->_post_email = trim($this->_pdo->getSetting('spotnabemail')); $this->_ssl_pubkey = $this->decompstr($this->_pdo->getSetting('spotnabsitepubkey')); $this->_ssl_prvkey = $this->decompstr($this->_pdo->getSetting('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->_pdo->getSetting('spotnabsitepubkey')); } return [ 'pubkey' => $this->_pdo->getSetting('spotnabsitepubkey'), 'prvkey' => $this->_pdo->getSetting('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); $_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': * 'Subject': * 'From': * 'Date': * 'Message-ID': * 'Bytes': * 'Lines': * 'Epoch': * ), * ... * ) * 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': , * 'key': , * 'user': , * 'email': , * 'ref': , * ), * array( * 'id': , * 'key': , * 'user': , * 'email': , * 'ref': , * ), * ) */ if(!count($group_hash)){ // Nothing to process return []; } // // 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[^<]+)<(?P[^>]+)>$/', $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 [$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': * 'Subject': * 'From': * 'Date': * 'Message-ID': * 'Bytes': * 'Lines': * 'Epoch': * ), * ... * ) */ // // 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[^<]+)<(?P[^>]+)>$/', $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 [$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); // 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 [] of associative [] 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 = []; // Header parsing for associative array returned $min_headers = ['Number', 'Subject', 'From', 'Date', 'Message-ID', 'Bytes', 'Lines' ]; do { $msgs = $this->_nntp->getOverview($range, true, false); // 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 []; if(!is_array($msgs))return []; // 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 = [$msgs]; } for($i=0;$i1) // 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 = [ 'server' => [ 'code' => $this->_post_site, 'title' => $this->_post_title, ], 'postdate_utc' => $this->local2utc($reftime_local), 'comments' => [] ]; // 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[^>]+)>/', $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->_pdo->getSetting('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 if (($this->_pdo->getSetting('alternate_nntp') == 1 ? $this->_nntp->doConnect(true, true) : $this->_nntp->doConnect()) !== true) { exit($this->_pdo->log->error("Unable to connect to usenet." . PHP_EOL)); } if($group !== Null) { // Reselect group if specified $summary = $this->_nntp->selectGroup($this->_post_group); 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 Settings(); $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 = [$header, $message]; if($debug){ // Append some debug data to the article $article[] = [ '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 = []; $ids = []; 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->_pdo->getSetting('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[] = [ // 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)? ['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( [ '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, ['config' => OPENSSL_CFG_PATH] ); // Get Public Key $details = openssl_pkey_get_details($res); if($details === false){ return false; } $pubkey = $details['key']; return [ '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))); } }