mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-31 18:28:54 +00:00
2290 lines
67 KiB
PHP
2290 lines
67 KiB
PHP
<?php
|
|
// Author l2g
|
|
// Date: Jan 27th, 2013
|
|
// Version: 0.92.1
|
|
//
|
|
// Description:
|
|
// This script is an implementation of an spotnet and newznab to which
|
|
// multiple servers can share comments and information amongst each other
|
|
//
|
|
// The implementation is based loosely on how spotweb works. If you don't know
|
|
// what spotweb is, feel free to Google it.
|
|
//
|
|
// With an spotnab, you have 2 options:
|
|
// 1. Fetch comments (and potential other things as we dream them up) from
|
|
// other newznab sources and apply them to your own comment section.
|
|
//
|
|
// Fetched content is scanned and decrypted using a password that the
|
|
// newznab server who posted it chooses to share with you. If you can't
|
|
// decrypt the post then you move along. Since all newznab servers choose
|
|
// to share and or not to share. Spotnab will only populate your database
|
|
// with comments from the sources who choose to share they're comments
|
|
// with you.
|
|
//
|
|
// A few public keys will be configured out of the box so that you can
|
|
// retrieve your comments from existing active sites should you choose
|
|
// to enable them.
|
|
//
|
|
// 2. Post comments; send all the comments made by your local users of your
|
|
// site to usenet encrypted by your own secure private key. Only those you
|
|
// share your public key will be allowed to decrypt it for their own
|
|
// server.
|
|
//
|
|
// Releases are uniquely bundled and shared across NewzNab servers through the
|
|
// use of the releases table. To uniquely identify a release across remote
|
|
// servers we need to extract the Message-ID from the first segment of the first
|
|
// file of the matched release (as it was posted to usenet). This ID is unique
|
|
// to usenet, so it'll be unique to us as well. We can achive this information
|
|
// from parsing the NZB file generated by the NewzNab server.
|
|
//
|
|
// This value can be certain to not conflict and uniquely identify a release
|
|
// across all NewzNab Servers.
|
|
//
|
|
// Content is posted and retrieved to Usnet in JSON encrypted using the key
|
|
// belonging to the NewzNab Server.
|
|
//
|
|
// Structure is as follows:
|
|
// {
|
|
// server: {
|
|
// code: <string>,
|
|
// title: <string>,
|
|
// },
|
|
// postdate_utc: 'YYYY-MM-DD hh:mm:ss.ms',
|
|
// comments: [
|
|
// {
|
|
// GID: <char[32]>,
|
|
// CID: <char[32]>,
|
|
// comment: <string>,
|
|
// is_visible: <bool>,
|
|
// username: <string>,
|
|
// postdate_utc: 'YYYY-MM-DD hh:mm:ss',
|
|
// }
|
|
// ]
|
|
// }
|
|
//
|
|
// server: Meta information on the server that posted the spot.
|
|
//
|
|
// server
|
|
// -/ code: This is taken from the users local database `site` table. It is
|
|
// the `code` the user configured their newznab server as.
|
|
//
|
|
// -/ title: This is also taken from the users local database `site` table. It
|
|
// is the the `title` the user configured their newznab server as.
|
|
//
|
|
// postdate_utc: This is the current time measured in the number of seconds since
|
|
// the Unix Epoch (January 1 1970 00:00:00 GMT). This represents
|
|
// the local UTC time of when this message was posted. It doesn't
|
|
// reflect the age of the content to follow.
|
|
//
|
|
// comments: This is an array of comment objects
|
|
//
|
|
// comments
|
|
// -/ GID: This is the Global Identifier for the release the comment
|
|
// is in relation to.
|
|
//
|
|
// -/ CID: This is a unique global identifier used to identify the comment
|
|
// itself. This allows the server to publish the comment again
|
|
// if there is an update to it's data (such as toggling it to a
|
|
// hidden type). This should be used to prevent a duplicate
|
|
// entry in the comments table when dealing with remote content.
|
|
//
|
|
// -/ comment: This is the actual comment itself associated with the release
|
|
//
|
|
// -/ is_visible: Whether or not the comment should be marked visible or not.
|
|
//
|
|
// -/ username: The username of the person who made the comment.
|
|
//
|
|
// -/ postdate_utc: The timestamp (in UTC) that the comment was created at.
|
|
//
|
|
|
|
|
|
/* The following database modifications are required for this to work:
|
|
|
|
DROP TABLE IF EXISTS `spotnab_sources`;
|
|
CREATE TABLE `spotnab_sources` (
|
|
`ID` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
`userName` VARCHAR(64) NOT NULL DEFAULT 'NNTP',
|
|
`userEmail` VARCHAR(128) NOT NULL DEFAULT 'SPOT@NNTP.COM',
|
|
`usenetGroup` VARCHAR(64) NOT NULL DEFAULT 'alt.binaries.test2',
|
|
`publicKey` VARCHAR(512) NOT NULL DEFAULT '',
|
|
`active` TINYINT(1) NOT NULL DEFAULT '1',
|
|
`description` VARCHAR(255) NULL DEFAULT '',
|
|
`lastUpdate` DATETIME DEFAULT NULL,
|
|
PRIMARY KEY (`ID`)
|
|
) ENGINE=MYISAM DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci AUTO_INCREMENT=1 ;
|
|
|
|
-- We must update releases table to track Global Identifier
|
|
ALTER TABLE `releases` ADD `GID` VARCHAR( 32 ) NULL AFTER `ID` ;
|
|
ALTER TABLE `releases` ADD INDEX `ix_releases_GID` ( `GID` );
|
|
|
|
-- We need a way of adding a signature to our posts so others can pull them if
|
|
-- they choose. The newznab code choosen is also very important (exists already
|
|
-- in the sites table) This is stamped with each push to the news server so
|
|
-- others can see where the post came from.
|
|
|
|
DELETE FROM `site` WHERE `setting` IN
|
|
('spotnabsitekey', 'spotnabsitepubkey', 'spotnabsiteprvkey', 'spotnabuser',
|
|
'spotnabemail', 'spotnabgroup', 'spotnabpost');
|
|
|
|
INSERT INTO `site` (`ID` , `setting` , `value` , `updateddate` )
|
|
VALUES ( NULL , 'spotnabsitepubkey', '', NOW( ));
|
|
INSERT INTO `site` (`ID` , `setting` , `value` , `updateddate` )
|
|
VALUES ( NULL , 'spotnabsiteprvkey', '', NOW( ));
|
|
INSERT INTO `site` (`ID` , `setting` , `value` , `updateddate` )
|
|
VALUES ( NULL , 'spotnabuser', 'NNTP', NOW( ));
|
|
INSERT INTO `site` (`ID` , `setting` , `value` , `updateddate` )
|
|
VALUES ( NULL , 'spotnabemail', 'SPOT@NNTP.COM', NOW( ));
|
|
INSERT INTO `site` (`ID` , `setting` , `value` , `updateddate` )
|
|
VALUES ( NULL , 'spotnabgroup', 'alt.binaries.test2', NOW( ));
|
|
|
|
-- We need a reference point we can use to track when we last did our updates to
|
|
-- the remote server... Set this value to '1' if you want to post your comments
|
|
-- to usenet for others to share from.
|
|
|
|
INSERT INTO `site` (`ID` , `setting` , `value` , `updateddate` )
|
|
VALUES ( NULL , 'spotnabpost', '1', NOW( ));
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- PHASE 2
|
|
-------------------------------------------------------------------------------
|
|
-- Merging `spotnab_comments` into `releasecomments`:
|
|
-- First prepare comments table:
|
|
-------------------------------------------------------------------------------
|
|
ALTER TABLE `releasecomment` ADD `sourceID` BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER `ID` ;
|
|
ALTER TABLE `releasecomment` ADD `GID` VARCHAR( 32 ) DEFAULT NULL AFTER `sourceID` ;
|
|
ALTER TABLE `releasecomment` ADD `CID` VARCHAR( 32 ) DEFAULT NULL AFTER `GID` ;
|
|
ALTER TABLE `releasecomment` ADD `username` VARCHAR( 50 ) DEFAULT NULL AFTER `userID` ;
|
|
ALTER TABLE `releasecomment` ADD `isVisible` TINYINT(1) DEFAULT 1 AFTER `text` ;
|
|
ALTER TABLE `releasecomment` ADD `isSynced` TINYINT(1) DEFAULT 0 AFTER `isVisible` ;
|
|
ALTER TABLE `releasecomment` ADD INDEX `ix_releasecomment_CID` ( `CID` );
|
|
ALTER TABLE `releasecomment` ADD INDEX `ix_releasecomment_GID` ( `GID` );
|
|
|
|
-- run this if the above sql statements are okay
|
|
DROP TABLE IF EXISTS `spotnab_comments`;
|
|
|
|
-- Things to consider with the table merge... releasecomments continues to work as normal
|
|
-- however new stuff fetched from usenet populate the sourceID, GID, and CID
|
|
-- Comments are now based on the GID (and no longer the releaseID); because of this we need
|
|
-- to gracefully phase out the releaseID column; This is done during the -g switch of this
|
|
-- tool which handles
|
|
|
|
INSTRUCTIONS
|
|
============
|
|
|
|
Both posting and fetching will 'NOT' work unless you've generated
|
|
Global Release ID values for all of your current releases... this is
|
|
done by running the command:
|
|
|
|
#> php spotnab.php -g
|
|
|
|
POSTING
|
|
1. To run all of the SQL statements identifed above
|
|
|
|
2. You 'must' have all Global Release ID's defined for each
|
|
release or the comments associated with it will not be
|
|
included in the post.
|
|
|
|
3. Generate yourself your own set of SSL keys:
|
|
#> php spotnab.php -k
|
|
|
|
4. At this point your ready to post... If you run the following SQL
|
|
statement, you'll mark all your existing comments:
|
|
|
|
-- Ensure spotnab posting is enabled:
|
|
UPDATE `site` set `value` = '1' WHERE `setting` = 'spotnabpost';
|
|
|
|
-- Posting actually uses this date as a reference to when
|
|
-- it last posted. So set it to a date so far back in time
|
|
-- that all your existing comments become declared as unposted.
|
|
|
|
UPDATE `site` set `updateddate` = '1980-01-01 00:00:00'
|
|
WHERE `setting` = 'spotnabpost' AND `value` = '1';
|
|
|
|
FETCHING
|
|
1. You need to acquire and enable sources. A source is as simple
|
|
as someone providing you their public key they generated using
|
|
the --keygen (-k) switch.
|
|
|
|
2. Fetch releases:
|
|
#> php spotnab.php -f
|
|
|
|
3. Consider fetching your own posts back just to test things out...:
|
|
|
|
SET @key := (SELECT value FROM site WHERE setting = 'spotnabsitepubkey');
|
|
INSERT INTO `spotnab_sources` (`ID` , `userName`, `userEmail` ,
|
|
`usenetGroup`, `publicKey` , `active` ,
|
|
`description` , `lastUpdate` )
|
|
VALUES ( NULL , 'NNTP', 'SPOT@NNTP.COM', 'alt.binaries.test2', @key,
|
|
'1', '1', 'My Key Fetch Test', NULL);
|
|
|
|
Then you can run the fetch release command again to poll for
|
|
stuff you posted already (if you did).
|
|
|
|
#> php spotnab.php -f
|
|
|
|
SHARING
|
|
1. Use the -k switch on the tool to retrieve your public key...
|
|
share it... let people use the comments posted on your site
|
|
get them to do the same to you. Eliminating crappy content
|
|
couldn't be more easy.
|
|
|
|
FUTURE CONSIDERATIONS
|
|
- Share NZB's and release content accumulated from a group so
|
|
people can pick and choose to fetch someone's already hard
|
|
earned processing power. Why have 1000 people process
|
|
the same group when 1 person could just share their results.
|
|
*/
|
|
|
|
require_once("config.php");
|
|
require_once(WWW_DIR."/lib/framework/db.php");
|
|
require_once(WWW_DIR."/lib/nntp.php");
|
|
require_once(WWW_DIR."/lib/nzb.php");
|
|
require_once(WWW_DIR."/lib/site.php");
|
|
require_once(WWW_DIR."/lib/releases.php");
|
|
|
|
// Subject Lines will always be a hash key that helps scanning identify whether
|
|
// or not the contents are valid or not. the hash key contains is built
|
|
// by applying the password key against the md5 some of the message content
|
|
// itself. Unmatched content is ignored.
|
|
|
|
$shortopts = "";
|
|
$shortopts .= "G";
|
|
$shortopts .= "g";
|
|
$shortopts .= "t";
|
|
$shortopts .= "p";
|
|
$shortopts .= "f";
|
|
$shortopts .= "k";
|
|
$shortopts .= "K";
|
|
|
|
$longopts = array(
|
|
"spotnab-post",
|
|
"spotnab-fetch",
|
|
"populate-gid",
|
|
"populate-fix-gid",
|
|
"keygen",
|
|
"force-keygen",
|
|
"test",
|
|
);
|
|
|
|
// Set Memory Restrictions To avoid running script if memory value is too low
|
|
// Minimum allowable memory set to 512MB
|
|
define("MIN_MEMORY", 536870912);
|
|
|
|
$options = getopt($shortopts, $longopts);
|
|
|
|
function display_help(){
|
|
echo "\n";
|
|
echo "SpotNab v0.92.1, Author: l2g\n";
|
|
echo "Syntax: spotnab.php <action>\n";
|
|
echo "\n";
|
|
echo "Actions:\n";
|
|
echo " -g, --populate-gid This could be considered phase one of "
|
|
."this project.\n";
|
|
echo " requiring that your releases database"
|
|
." table is up to date\n";
|
|
echo " with all GID (Global Identifiers) so"
|
|
." it can correctly\n";
|
|
echo " communicate with other servers that "
|
|
."share the same content\n";
|
|
echo " -G, --populate-fix-gid Same as -g except broken nzb files are "
|
|
."also broken releases.\n";
|
|
echo " Specifying this switch will remove "
|
|
."these dead releses.\n";
|
|
echo " -f, --spotnab-fetch Get latest spotnab content from "
|
|
."usenet using the information\n";
|
|
echo " from the servers configured.\n";
|
|
echo " -p, --spotnab-post Post latest updates from local system "
|
|
."to usenet.\n";
|
|
echo "\n";
|
|
echo " -t, --test Produces a whole lot of garbage, but "
|
|
."is used for testing\n";
|
|
echo " the internals of the class...\n";
|
|
echo " -k, --keygen Generate a new SSL Public/Private Key "
|
|
."pair only if one isn't\n";
|
|
echo " already generated.\n";
|
|
echo " -K, --force-keygen Generate a new SSL Public/Private Key "
|
|
."pair\n";
|
|
echo "\n";
|
|
|
|
$max_mem = get_max_mem();
|
|
if ($max_mem < MIN_MEMORY){
|
|
echo "Warning: your memory is very low. Consider running this script as:\n";
|
|
echo " #> php -d memory_limit=2G spotnab.php <action>\n";
|
|
echo "\n";
|
|
}
|
|
}
|
|
|
|
// Silent Error Handler (used to shut up noisy XML exceptions)
|
|
// We don't care if the nzb is corrupt with so many additional lines of
|
|
// info... that is someone elses problem and defeats the purpose
|
|
// and readability of this script... we output a more relaxed
|
|
// error in these events
|
|
// 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);
|
|
}
|
|
|
|
function get_max_mem(){
|
|
// returns the current memory allocated for this process
|
|
$val = trim(ini_get('memory_limit'));
|
|
$last = strtolower($val[strlen($val)-1]);
|
|
switch($last) {
|
|
// The 'G' modifier is available since PHP 5.1.0
|
|
case 'g':
|
|
$val *= 1024;
|
|
case 'm':
|
|
$val *= 1024;
|
|
case 'k':
|
|
$val *= 1024;
|
|
}
|
|
return $val;
|
|
}
|
|
// Useful for debugging hex
|
|
function hex_dump($data, $newline="\n")
|
|
{
|
|
static $from = '';
|
|
static $to = '';
|
|
static $width = 16; # number of bytes per line
|
|
static $pad = '.'; # padding for non-visible characters
|
|
|
|
if ($from===''){
|
|
for ($i=0; $i<=0xFF; $i++)
|
|
{
|
|
$from .= chr($i);
|
|
$to .= ($i >= 0x20 && $i <= 0x7E) ? chr($i) : $pad;
|
|
}
|
|
}
|
|
|
|
$hex = str_split(bin2hex($data), $width*2);
|
|
$chars = str_split(strtr($data, $from, $to), $width);
|
|
|
|
$offset = 0;
|
|
$data_out = '';
|
|
foreach ($hex as $i => $line){
|
|
$data_out .= sprintf('%6X',$offset).' : '.
|
|
implode(' ', str_split($line,2)) .
|
|
' [' . $chars[$i] . ']' . $newline;
|
|
$offset += $width;
|
|
}
|
|
return $data_out;
|
|
}
|
|
|
|
// 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.
|
|
const FETCH_SUBJECT_REGEX = '/^(?P<checksum>[0-9a-z]{40})-(?P<utcref>[0-9]{14})$/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
|
|
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 = 25000;
|
|
|
|
// Maximum age (in seconds) we look back for a source
|
|
const FETCH_MAXIMUM_AGE = 172800;
|
|
|
|
// 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 like a.b.boneless 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.
|
|
const SSL_MAX_BUF_LEN = 245;
|
|
|
|
// 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 = ':::';
|
|
|
|
private $_nntp;
|
|
|
|
// Meta Information is fetched from DB
|
|
private $_post_site;
|
|
private $_post_title;
|
|
|
|
private $_post_user;
|
|
private $_post_email;
|
|
private $_post_key;
|
|
private $_post_group;
|
|
|
|
/* SSL Public & Private Keys */
|
|
private $_ssl_pubkey;
|
|
private $_ssl_prvkey;
|
|
|
|
/* Encryption Variables */
|
|
private $_cypher_adjust;
|
|
private $_cypher_modulus;
|
|
|
|
/* Maximum of 3 deep encryption bases */
|
|
private $_cypher_scramble;
|
|
|
|
// **********************************************************************
|
|
function __construct($post_user=Null, $post_email=Null, $post_key=Null,
|
|
$post_group=Null) {
|
|
/*
|
|
Constructor
|
|
*/
|
|
$this->_nntp = new NNTP();
|
|
|
|
$this->_post_user = $post_user;
|
|
$this->_post_email = $post_email;
|
|
$this->_post_key = $post_key;
|
|
$this->_post_group = $post_group;
|
|
|
|
$db = new DB();
|
|
|
|
// Fetch Meta information
|
|
$res = $db->queryOneRow("SELECT value FROM site WHERE "
|
|
."setting = 'code'");
|
|
$this->_post_site = ($res !== false)?$res['value']:Null;
|
|
$res = $db->queryOneRow("SELECT value FROM site WHERE "
|
|
."setting = 'title'");
|
|
$this->_post_title = ($res !== false)?$res['value']:Null;
|
|
|
|
if ($this->_post_user === Null){
|
|
// Fetch the SpotNab EmailID
|
|
$res = $db->queryOneRow("SELECT value FROM site WHERE "
|
|
."setting = 'spotnabuser'");
|
|
$this->_post_user = ($res !== false)?$res['value']:Null;
|
|
}
|
|
if ($this->_post_email === Null){
|
|
// Fetch the SpotNab EmailID
|
|
$res = $db->queryOneRow("SELECT value FROM site WHERE "
|
|
."setting = 'spotnabemail'");
|
|
$this->_post_email = ($res !== false)?$res['value']:Null;
|
|
}
|
|
if ($this->_post_key === Null){
|
|
// Fetch the SpotNab Decrypt/Encrypt Key
|
|
$res = $db->queryOneRow("SELECT value FROM site WHERE "
|
|
."setting = 'spotnabsitekey'");
|
|
$this->_post_key = ($res !== false)?$res['value']:Null;
|
|
}
|
|
if ($this->_post_group === Null){
|
|
// Fetch the SpotNab Usenet Group to post to
|
|
$res = $db->queryOneRow("SELECT value FROM site WHERE "
|
|
."setting = 'spotnabgroup'");
|
|
$this->_post_group = ($res !== false)?$res['value']:Null;
|
|
}
|
|
|
|
// Fetch our keys
|
|
$res = $db->queryOneRow("SELECT value FROM site WHERE "
|
|
."setting = 'spotnabsitepubkey'");
|
|
$this->_ssl_pubkey = ($res !== false)?trim($res['value']):false;
|
|
if($this->_ssl_pubkey)
|
|
$this->_ssl_pubkey = $this->decompstr($this->_ssl_pubkey);
|
|
$res = $db->queryOneRow("SELECT value FROM site WHERE "
|
|
."setting = 'spotnabsiteprvkey'");
|
|
$this->_ssl_prvkey = ($res !== false)?trim($res['value']):false;
|
|
if($this->_ssl_prvkey)
|
|
$this->_ssl_prvkey = $this->decompstr($this->_ssl_prvkey);
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function fetch($reftime=Null, $comments=true, $retries=3){
|
|
/*
|
|
* This function queries all enabled sources and fetches any content
|
|
* they are sharing.
|
|
*/
|
|
|
|
$db = new DB();
|
|
$last = $first = Null;
|
|
|
|
// Return Value; Initialize it to Okay
|
|
// we'll change it to false if we have to.
|
|
$fetch_okay = true;
|
|
|
|
if($reftime === Null){
|
|
// Fetch local time (but look back 1 day)
|
|
$reftime = $this->utc2local((time()-(60*60*24)));
|
|
}
|
|
|
|
// First we find all active sources and build a hash table we can
|
|
// use to simplify fetching.
|
|
$sql = "SELECT * FROM spotnab_sources WHERE active = 1 ".
|
|
"ORDER BY usenetGroup,lastUpdate DESC";
|
|
$res = $db->query($sql);
|
|
$group_hash = array();
|
|
|
|
if(!count($res)){
|
|
printf("%s WARNING - There are no source(s) defined to fetch".
|
|
" from.\n", date("Y-m-d H:i:s"));
|
|
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();
|
|
}
|
|
|
|
// 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 = $source['lastUpdate'];
|
|
|
|
if(!$ref){
|
|
// We've never fetched from the group before, so we'll use
|
|
// the reftime passed to the function
|
|
$ref = $reftime;
|
|
}
|
|
|
|
$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),
|
|
);
|
|
}
|
|
|
|
// We want to resort the internal arrays by they're ref time
|
|
// so that the oldest (longest without an update) is processed
|
|
// first
|
|
|
|
// We set a cap on how many days in the past we look
|
|
$_max_age = time() - SpotNab::FETCH_MAXIMUM_AGE;
|
|
|
|
foreach(array_keys($group_hash) as $key){
|
|
$_ref = array();
|
|
foreach($group_hash[$key] as $id => $source){
|
|
# Source Time (within reason)
|
|
$_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{
|
|
$this->_nntp->doConnect();
|
|
}
|
|
catch(Exception $e){
|
|
printf("%s ERROR - Failed to connect to Usenet\n",
|
|
date("Y-m-d H:i:s"));
|
|
return false;
|
|
}
|
|
|
|
foreach($group_hash as $group => $hash){
|
|
printf("%s INFO - Fetching $group for %d source(s).\n",
|
|
date("Y-m-d H:i:s"), count($hash));
|
|
|
|
$summary = $this->_nntp->selectGroup($group);
|
|
while(PEAR::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("%s ERROR - Exausted connect/fetch retries "
|
|
."attempts; aborting.\n",
|
|
date("Y-m-d H:i:s"));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 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){
|
|
// Fail
|
|
printf("%s WARNING - Could not determine starting article. "
|
|
."skipping group.\n", date("Y-m-d H:i:s"));
|
|
continue;
|
|
}
|
|
|
|
// Group Processing Initialization
|
|
$processed = 0;
|
|
$batch = $last = intval($summary['last']);
|
|
$total = $last-$first;
|
|
|
|
// Track how many records were inserted, updated
|
|
$inserted = 0;
|
|
$updated = 0;
|
|
|
|
printf("%s INFO - Fetching records(s) ", date("Y-m-d H:i:s"));
|
|
// 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($group,
|
|
"$first-$batch", $retries);
|
|
|
|
if($headers === false){
|
|
// Retry Atempts exausted
|
|
$fetch_okay = false;
|
|
break;
|
|
}
|
|
|
|
// Process the header batch
|
|
$saved = $this->process_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;
|
|
}
|
|
}
|
|
echo " Done\n";
|
|
|
|
printf("%s INFO - Results: %d new and %d updated comment(s).\n",
|
|
date("Y-m-d H:i:s"), $inserted, $updated);
|
|
}
|
|
// Restore handler
|
|
restore_error_handler();
|
|
|
|
// Ensure We're not connected
|
|
try{$this->_nntp->doQuit();}
|
|
catch(Exception $e)
|
|
{/* do nothing */}
|
|
|
|
return $fetch_okay;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
private function _first_article_by_date($group, $refdate, $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;
|
|
|
|
if(is_string($refdate)){
|
|
// Convert to Integer (Local Time)
|
|
$refdate = strtotime($refdate);
|
|
}
|
|
|
|
printf("%s INFO - Scanning %s for start article",
|
|
date("Y-m-d H:i:s"), $group);
|
|
|
|
while(($retries > 0) && ($interval > 0)){
|
|
$summary = $this->_nntp->selectGroup($group);
|
|
if(PEAR::isError($summary))
|
|
{
|
|
// Reset Connection
|
|
$this->_nntpReset();
|
|
|
|
// Track retry attempts
|
|
$retries--;
|
|
if($retries <= 0){
|
|
// Retry Atempts exausted
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
$last = intval($summary['last']);
|
|
$first = intval($summary['first']);
|
|
|
|
$curdate = $lastdate = Null;
|
|
$curid = $lastid = false;
|
|
$interval = 0;
|
|
while($retries > 0){
|
|
|
|
// 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']);
|
|
if (($last-$lastid) > SpotNab::FETCH_MAXIMUM_HEADERS){
|
|
// We exceeded our maximum header limit
|
|
// adjust accordingly
|
|
$lastid = $last - SpotNab::FETCH_MAXIMUM_HEADERS;
|
|
}
|
|
// Return pointer
|
|
echo " ".($last-$lastid)." record(s) back.\n";
|
|
return $lastid;
|
|
}
|
|
|
|
// Swap
|
|
$lastdate = $curdate;
|
|
$lastid = $curid;
|
|
|
|
$msgs = $this->_get_headers(
|
|
$group, ($last-$interval), $retries);
|
|
|
|
if($msgs === false){
|
|
// fail (retry limit exceeded)
|
|
echo "Failed.\n";
|
|
return false;
|
|
}
|
|
|
|
// Reset Miss Count
|
|
$misses = 0;
|
|
|
|
// Save Tracker
|
|
$curdate = strtotime($msgs[0]['Date']);
|
|
$curid = intval($msgs[0]['Number']);
|
|
|
|
//echo "\nCUR:".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...
|
|
|
|
if($refdate > $curdate && $refdate > $lastdate){
|
|
$last = intval($summary['last']);
|
|
if (($last-$curid) > SpotNab::FETCH_MAXIMUM_HEADERS){
|
|
// We exceeded our maximum header limit
|
|
// adjust accordingly
|
|
$curid = $last - SpotNab::FETCH_MAXIMUM_HEADERS;
|
|
}
|
|
// Found content
|
|
echo " ".($last-$curid)." record(s) back.\n";
|
|
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_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 DB();
|
|
|
|
// Comments
|
|
// old: $sql_new_cmt = "INSERT INTO spotnab_comments (".
|
|
// old: "ID, sourceID, username, releaseHash, commentHash, isVisible, ".
|
|
// old: "comment, dateCreated, isSynced) VALUES (".
|
|
// old: "NULL, %d, %s, %s, %s, %d, %s, %s, NOW())";
|
|
// old: $sql_upd_cmt = "UPDATE spotnab_comments SET ".
|
|
// old: "isVisible = %d, comment = %s, isSynced = 1".
|
|
// old: "WHERE sourceID = %d AND releaseHash = %s AND commentHash = %s";
|
|
// old: $sql_fnd_cmt = "SELECT count(ID) as cnt FROM spotnab_comments ".
|
|
// old: "WHERE sourceID = %d AND releaseHash = %s AND commentHash = %s";
|
|
|
|
$sql_new_cmt = "INSERT INTO releasecomment (".
|
|
"ID, sourceID, username, userID, GID, CID, isVisible, ".
|
|
"`text`, createddate, isSynced) VALUES (".
|
|
"NULL, %d, %s, 0, %s, %s, %d, %s, %s, 1)";
|
|
$sql_upd_cmt = "UPDATE releasecomment SET ".
|
|
"isVisible = %d, `text` = %s".
|
|
"WHERE sourceID = %d AND GID = %s AND CID = %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 spotnab_sources 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.
|
|
|
|
// Make sure the Message-ID is posted using the fixed SEGID_DOMAIN
|
|
// <blabl.12423@SEGID_DOMAIN>
|
|
if(!preg_match('/^<[^@]+@(?P<segid>.*)>$/',
|
|
$header['Message-ID'], $matches))
|
|
continue;
|
|
if($matches['segid'] != SpotNab::SEGID_DOMAIN)
|
|
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_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'])){
|
|
|
|
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 '.';
|
|
$res = $db->query(sprintf($sql_upd_cmt,
|
|
$is_visible,
|
|
$db->escapeString($comment['comment']),
|
|
$hash['ID'],
|
|
$db->escapeString($comment['GID']),
|
|
$db->escapeString($comment['CID'])
|
|
));
|
|
$updates += ($db->getAffectedRows()>0)?1:0;
|
|
}else{
|
|
// Make some noise
|
|
echo '+';
|
|
// Perform Insert
|
|
$res = @$db->query(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']))
|
|
));
|
|
$inserts += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update spotnab_sources table, set lastUpdate to the
|
|
// timestamp parsed from the header.
|
|
$db->query(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);
|
|
}
|
|
|
|
// ***********************************************************************
|
|
private 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(PEAR::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;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
private 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(PEAR::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
|
|
// PEAR::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, $comments=true, $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*
|
|
*/
|
|
$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
|
|
$message['comments'] = $this->unPostedComments($reftime_local);
|
|
|
|
$db = new DB();
|
|
$sql = sprintf("UPDATE releasecomment "
|
|
."SET isSynced = 1 WHERE "
|
|
."releaseID != 0 "
|
|
."AND userID != 0 "
|
|
."AND sourceID = 0 "
|
|
."AND isSynced = 0");
|
|
|
|
//$sql = "UPDATE site SET updateddate = NOW( ) ".
|
|
// "WHERE setting = 'spotnabpost' ";
|
|
|
|
printf("%s INFO - Detected %d new comment(s) ...\n",
|
|
date("Y-m-d H:i:s"), count($message['comments']));
|
|
|
|
// Later this block of code will look like this:
|
|
// if($comments || $releases || ... )
|
|
if(!is_array($message['comments'])){
|
|
// nothing to post; update database to safe future
|
|
// fetching time
|
|
$res = @$db->query($sql);
|
|
return true;
|
|
}
|
|
|
|
if(!($this->_ssl_prvkey && $this->_ssl_pubkey))
|
|
{
|
|
printf("%s ERROR - Posting not possible without generated "
|
|
."SSL keys ...", date("Y-m-d H:i:s"));
|
|
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("%s INFO - Posting article ...", date("Y-m-d H:i:s"));
|
|
if ($this->_postArticle($article, $retries))
|
|
{
|
|
// Post is good; update database
|
|
$res = @$db->query($sql);
|
|
echo "Done.\n";
|
|
return true;
|
|
}
|
|
echo "Failed.\n";
|
|
|
|
// If code reached here, then we failed to post content
|
|
return false;
|
|
|
|
}
|
|
|
|
// ***********************************************************************
|
|
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
|
|
$this->_nntp->doConnect();
|
|
while($retries > 0)
|
|
{
|
|
try
|
|
{
|
|
$summary = $this->_nntp->selectGroup($this->_post_group);
|
|
if(PEAR::isError($summary)){
|
|
$summary = $this->_nntpReset($this->_post_group);
|
|
$retries--;
|
|
continue;
|
|
}
|
|
// Check if server will receive an article
|
|
$_err = $this->_nntp->cmdPost();
|
|
if (PEAR::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
|
|
$summary = $this->_nntp->selectGroup($this->_post_group);
|
|
if(PEAR::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;
|
|
}
|
|
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{$this->_nntp->doConnect();}
|
|
catch(Exception $e){return false;}
|
|
|
|
if($group !== Null)
|
|
{
|
|
// Reselect group if specified
|
|
$summary = $this->_nntp->selectGroup($this->_post_group);
|
|
if(PEAR::isError($summary))
|
|
return false;
|
|
return $summary;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function nzbToGID($nzbfile){
|
|
/*
|
|
Scans an nzb file specified and returns the global identifer (GID)
|
|
|
|
Null is returned if there is a problem.
|
|
*/
|
|
|
|
// Scans an individual NZB file for nfo information
|
|
$filename = basename($nzbfile);
|
|
|
|
// Extract NZB Content (gzip supported)
|
|
ob_start();
|
|
@readgzfile($nzbfile);
|
|
$nzbdata = ob_get_contents();
|
|
ob_end_clean();
|
|
|
|
// Calculate how many bytes we have read into memory
|
|
// and compare them against how much free memory we have left...
|
|
// if there is a discrepency, we'll need to write the content back
|
|
// to disk uncompressed and read it back in a buffered version
|
|
$bytes = strlen($nzbdata)."\n";
|
|
$free = memory_get_usage(true);
|
|
if(($free - $bytes) < (1024*1024*10))
|
|
{
|
|
// Work with cache files instead of memory to avoid overflowing
|
|
$cache = tmpfile();
|
|
fwrite($cache, $nzbdata);
|
|
fseek($cache, 0);
|
|
// Free Memory
|
|
unset($nzbdata);
|
|
|
|
// Read header information
|
|
$nzbdata = fread($cache, 1024);
|
|
|
|
// Gets rid of all namespace definitions
|
|
$nzbdata = preg_replace('/xmlns[^=]*="[^"]*"/i', "", $nzbdata, 1);
|
|
// Gets rid of all namespace references
|
|
$nzbdata = preg_replace("/[a-zA-Z]+:([a-zA-Z]+[=>])/", "$1", $nzbdata);
|
|
// Read in the rest of the file now
|
|
while (!feof($cache))
|
|
$nzbdata .= fread($cache, 8192);
|
|
fclose($cache);
|
|
}
|
|
else
|
|
{
|
|
// Gets rid of all namespace definitions
|
|
$nzbdata = preg_replace('/xmlns[^=]*="[^"]*"/i', "", $nzbdata, 1);
|
|
// Gets rid of all namespace references
|
|
$nzbdata = preg_replace("/[a-zA-Z]+:([a-zA-Z]+[=>])/", "$1", $nzbdata);
|
|
}
|
|
|
|
// Set error Handler
|
|
set_error_handler('snHandleError');
|
|
try{
|
|
$xml = new SimpleXMLElement($nzbdata);
|
|
}catch(Exception $e){
|
|
// unparseable
|
|
return Null;
|
|
}
|
|
// NZB Information is no longer needed now
|
|
// free it's memory up
|
|
unset($nzbdata);
|
|
|
|
// Restore handler as any future errors really are... code errors :)
|
|
restore_error_handler();
|
|
|
|
// Fetch First Segment of the first set of segments of first file
|
|
$segment = $xml->xpath("//file[1]/segments[1]/segment[1]");
|
|
if(is_array($segment) && !count($segment))
|
|
{
|
|
// This happens if a nzb file exists with no segment entries
|
|
// This is a critical bug some-where else in the code, but we can
|
|
// never post GID's using this information
|
|
return Null;
|
|
}
|
|
$segment = "".(is_array($segment)?$segment[0]:$segment);
|
|
if(empty($segment))
|
|
return Null;
|
|
|
|
// Return Global Identifer
|
|
return md5($segment);
|
|
}
|
|
|
|
// ***********************************************************************
|
|
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->decodeYenc($message);
|
|
|
|
// Decompress Messsage
|
|
$message = @gzuncompress($message);
|
|
|
|
if ($key === Null)
|
|
$key = $this->_post_key;
|
|
|
|
// Decrypt Message
|
|
if($decrypt){
|
|
$message = $this->decrypt($message, $key);
|
|
if($message === false){
|
|
// fail
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
/*
|
|
|
|
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");
|
|
|
|
$msgid = sprintf('<%s.%d@%s>',
|
|
$this->getRandomStr(32),
|
|
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'
|
|
// );
|
|
|
|
// Convert to base64
|
|
$message = base64_encode($message);
|
|
|
|
// Encrypt Message
|
|
$message = $this->encrypt($message);
|
|
if($message === false){
|
|
// fail
|
|
return false;
|
|
}
|
|
|
|
// Compress Messsage
|
|
$message = @gzcompress($message, 9);
|
|
|
|
// Yenc Binary Content
|
|
$message = $this->encodeYenc($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
|
|
);
|
|
|
|
$site = new Sites();
|
|
$header = "Subject: " . $subject . "\r\n";
|
|
$header .= "Newsgroups: " . $this->_post_group . "\r\n";
|
|
$header .= "Message-ID: $msgid\r\n";
|
|
$header .= "X-Newsreader: NewzNab v" . $site->version() ."\r\n";
|
|
$header .= "X-No-Archive: yes\r\n";
|
|
|
|
$user = trim($this->_post_user);
|
|
$email = trim($this->_post_email);
|
|
|
|
$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>', $this->_post_user,
|
|
$this->_post_email),
|
|
'Date' => date(DATE_RFC822, strtotime(
|
|
$this->utc2local($reftime))),
|
|
'Message-ID' => $msgid,
|
|
'Bytes' => strlen($message),
|
|
'Lines' => '1',
|
|
'Epoch' => strtotime($this->utc2local($reftime)),
|
|
'Group' => 'alt.binaries.test2'
|
|
);
|
|
}
|
|
return $article;
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function unPostedComments() {
|
|
/*
|
|
* 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 DB();
|
|
|
|
// Make sure we ca npost
|
|
$sql = "SELECT count(ID) as cnt FROM site "
|
|
."WHERE setting = 'spotnabpost' AND value = '1'";
|
|
|
|
$res = $db->queryOneRow($sql);
|
|
if(!$res)
|
|
// Nothing, return empty array list
|
|
return Null;
|
|
if(!intval($res['cnt']))
|
|
// Disabled
|
|
return Null;
|
|
|
|
// 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 "
|
|
."LEFT JOIN releases r ON r.ID = rc.releaseID AND rc.releaseID != 0 "
|
|
."LEFT JOIN users u ON rc.userID = u.ID AND rc.userID != 0 "
|
|
."WHERE r.GID IS NOT NULL "
|
|
."AND sourceID = 0 AND isSynced = 0");
|
|
|
|
$res = $db->query($sql);
|
|
if(!$res)
|
|
return Null;
|
|
|
|
// Now we prepare a comments array to return with
|
|
$comments = 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;
|
|
|
|
// Hash a unique Comment ID to associate with this message
|
|
$CID = md5($comment['ID'].$comment['username'].$comment['createddate'].$comment['host']);
|
|
$comments[] = array(
|
|
// Release Global ID
|
|
'GID' => $comment['GID'],
|
|
// Comment ID
|
|
'CID' => $CID,
|
|
// Store comment
|
|
'comment' => $comment['text'],
|
|
// Store comment
|
|
'username' => $comment['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: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);
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function encodeYenc($message, $filename, $linelen = 128,
|
|
$crc32 = true)
|
|
{
|
|
/* This code was found http://everything2.com/title/yEnc+PHP+Class
|
|
* It could probably get placed into the NNTP() code with it's
|
|
* partner decodeYenc()
|
|
*/
|
|
// yEnc 1.3 draft doesn't allow line lengths of more than 254 bytes.
|
|
if ($linelen > 254)
|
|
$linelen = 254;
|
|
|
|
if ($linelen < 1)
|
|
return false;
|
|
|
|
$encoded = "";
|
|
|
|
// Encode each character of the message one at a time.
|
|
for( $i = 0; $i < strlen($message); $i++)
|
|
{
|
|
$value = (ord($message{$i}) + 42) % 256;
|
|
|
|
// Escape NULL, TAB, LF, CR, space, . and = characters.
|
|
if ($value == 0 || $value == 9 || $value == 10 ||
|
|
$value == 13 || $value == 32 || $value == 46 ||
|
|
$value == 61)
|
|
$encoded .= "=".chr(($value + 64) % 256);
|
|
else
|
|
$encoded .= chr($value);
|
|
}
|
|
|
|
// Wrap the lines to $linelen characters
|
|
// TODO: Make sure we don't split escaped characters in half, as per the yEnc spec.
|
|
$encoded = trim(chunk_split($encoded, $linelen));
|
|
|
|
// Tack a yEnc header onto the encoded message.
|
|
$encoded = "=ybegin line=$linelen size=".strlen($message)
|
|
." name=".trim($filename)."\r\n".$encoded;
|
|
$encoded .= "\r\n=yend size=".strlen($message);
|
|
|
|
// Add a CRC32 checksum if desired.
|
|
if ($crc32 === true)
|
|
$encoded .= " crc32=".strtolower(sprintf("%04X", crc32($message)));
|
|
|
|
return $encoded."\r\n";
|
|
}
|
|
|
|
// ***********************************************************************
|
|
public function keygen($passphrase=Null, $bits=2048,
|
|
$type=OPENSSL_KEYTYPE_RSA)
|
|
{
|
|
//Generate Key
|
|
$res = openssl_pkey_new(
|
|
array(
|
|
'private_key_bits' => $bits,
|
|
'private_key_type' => $type
|
|
)
|
|
);
|
|
|
|
// Get Private Key
|
|
openssl_pkey_export($res, $prvkey, $passphrase);
|
|
|
|
// 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;
|
|
|
|
// 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 .= $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;
|
|
|
|
$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;
|
|
$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);
|
|
}
|
|
}
|
|
|
|
if(!$options){ display_help(); exit(1);}
|
|
if(!count($options)){ display_help(); exit(1);}
|
|
|
|
$max_mem = get_max_mem();
|
|
if ($max_mem < MIN_MEMORY){
|
|
echo "Warning: your memory is very low. Consider running this script as:\n";
|
|
echo " #> php -d memory_limit=2G spotnab.php <action>\n";
|
|
}
|
|
|
|
if(array_key_exists("t", $options) ||
|
|
array_key_exists("test", $options)){
|
|
$sn = new SpotNab();
|
|
|
|
printf("%s INFO - Testing SSL Key Generator ...",
|
|
date("Y-m-d H:i:s"));
|
|
$keys = $sn->keygen();
|
|
if(is_array($keys) &&
|
|
array_key_exists("pubkey", $keys) &&
|
|
array_key_exists("prvkey", $keys))
|
|
{
|
|
$prvkey = $sn->decompstr($keys['prvkey']);
|
|
$pubkey = $sn->decompstr($keys['pubkey']);
|
|
$refc = $sn->getRandomStr(80);
|
|
$refd = $sn->decrypt($sn->encrypt($refc, $prvkey), $pubkey);
|
|
echo ($refc == $refd)?"Successful!\n":"Failed!\n";
|
|
}else{
|
|
echo "Failed!\n";
|
|
}
|
|
|
|
printf("%s INFO - Testing SSL encryption/decryption ...",
|
|
date("Y-m-d H:i:s"));
|
|
$preMsg = $sn->getRandomStr(800);
|
|
$postMsg = $sn->decrypt($sn->encrypt($preMsg));
|
|
if($postMsg === false){
|
|
echo "Failed!\n";
|
|
}else if(!strcmp($preMsg, $postMsg)){
|
|
echo "Successful!\n";
|
|
}else{
|
|
echo "Failed!\n";
|
|
}
|
|
|
|
printf("%s INFO - Testing message encode/decode ...",
|
|
date("Y-m-d H:i:s"));
|
|
$before = array(
|
|
'server' => array(
|
|
'code' => 'l2g',
|
|
'title' => 'l2g newznab'
|
|
),
|
|
'postdate_utc' => $sn->local2utc(),
|
|
'comments' => array(
|
|
array(
|
|
'GID' => 'ABCDEFHIJKLMNOPQRSTUVWXYZ0123456',
|
|
'CID' => 'ABCDEFHIJKLMNOPQRSTUVWXYZ0123456',
|
|
'comment' => 'I just saw l2g blow his nose in his shirt; thats disgusting.',
|
|
'username' => 'bb',
|
|
'is_visible' => 1,
|
|
'postdate_utc' => $sn->local2utc(time()-86400)
|
|
),
|
|
array(
|
|
'GID' => 'ABCDEFHIJKLMNOPQRSTUVWXYZ0123456',
|
|
'CID' => 'ABCDEFHIJKLMNOPQRSTUVWXYZ0123456',
|
|
'username' => 'sy',
|
|
'is_visible' => 1,
|
|
'comment' => 'That is really disgusting... l2g is such a dick.',
|
|
'postdate_utc' => $sn->local2utc(time()-86000)
|
|
)
|
|
)
|
|
);
|
|
$article = $sn->encodePost($before, Null, true);
|
|
if($article !== false){
|
|
$after = $sn->decodePost($article[1]);
|
|
if($before === $after){
|
|
echo "Successful!\n";
|
|
}else{
|
|
echo "Failed!\n";
|
|
}
|
|
}else{
|
|
echo "Failed!\n";
|
|
}
|
|
|
|
printf("%s INFO - Testing UTC/Local conversions [1/4]...",
|
|
date("Y-m-d H:i:s"));
|
|
$refa = $sn->utc2local();
|
|
$refb = $sn->utc2local($sn->local2utc($refa));
|
|
echo ($refa == $refb)?"Successful!\n":"Failed!\n";
|
|
printf("%s INFO - Testing UTC/Local conversions [2/4]...",
|
|
date("Y-m-d H:i:s"));
|
|
$refa = $sn->local2utc();
|
|
$refb = $sn->local2utc($sn->utc2local($refa));
|
|
echo ($refa == $refb)?"Successful!\n":"Failed!\n";
|
|
printf("%s INFO - Testing UTC/Local conversions [3/4]...",
|
|
date("Y-m-d H:i:s"));
|
|
$refa = $sn->utc2local(time());
|
|
$refb = $sn->utc2local($sn->local2utc($refa));
|
|
echo ($refa == $refb)?"Successful!\n":"Failed!\n";
|
|
printf("%s INFO - Testing UTC/Local conversions [4/4]...",
|
|
date("Y-m-d H:i:s"));
|
|
$refa = $sn->local2utc(time());
|
|
$refb = $sn->local2utc($sn->utc2local($refa));
|
|
echo ($refa == $refb)?"Successful!\n":"Failed!\n";
|
|
|
|
printf("%s INFO - Testing fake usenet parse ...",
|
|
date("Y-m-d H:i:s"));
|
|
// Fake group hash table
|
|
$hash = array(array(
|
|
'ID' => 0,
|
|
'key' => $sn->decompstr($keys['pubkey']),
|
|
'user' => 'NNTP',
|
|
'email' => 'SPOT@NNTP.COM',
|
|
// We want to find new content, so to make our header
|
|
// new, we need to take our ref time and back down
|
|
// one second so it can be processed..
|
|
'ref' => $article[2]['Epoch']-1
|
|
));
|
|
|
|
// Fake headers (use debug information from encodePost)
|
|
$headers = array($article[2]);
|
|
|
|
$matched = $sn->process_headers($headers, $hash, false);
|
|
if($matched !== false){
|
|
$inserted = $matched[0];
|
|
$updated = $matched[1];
|
|
echo ($matched > 0)?"Successful!\n":"Failed!\n";
|
|
}else{
|
|
echo "Failed\n";
|
|
}
|
|
}
|
|
|
|
$delete_broken_releases = false;
|
|
if(array_key_exists("G", $options) ||
|
|
array_key_exists("populate-fix-gid", $options)){
|
|
// Toggle Broken Release Fix
|
|
$delete_broken_releases = true;
|
|
// Force populate-gid cleanup
|
|
$options['populate-gid'] = true;
|
|
}
|
|
|
|
if(array_key_exists("g", $options) ||
|
|
array_key_exists("populate-gid", $options)){
|
|
$db = new DB();
|
|
$nzb = new NZB();
|
|
$sn = new SpotNab();
|
|
|
|
$processed = 0;
|
|
|
|
printf("%s INFO - Populating GID in releases table.\n", date("Y-m-d H:i:s"));
|
|
$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";
|
|
$offset = 0;
|
|
while(1){
|
|
$res = $db->query(sprintf($fsql, $offset, 5000));
|
|
if(!($res && count($res) > 0))
|
|
break;
|
|
|
|
foreach ($res as $r){
|
|
$nzbfile = $nzb->getNZBPath($r["guid"]);
|
|
if($nzbfile === Null){
|
|
printf("\n%s WARNING - Could not extract NZB file "
|
|
."information for: %d/%s ...\n", date("Y-m-d H:i:s"),
|
|
intval($r["ID"]), $r["name"]);
|
|
$offset++;
|
|
continue;
|
|
}
|
|
|
|
if(!is_file($nzbfile)){
|
|
if($delete_broken_releases){
|
|
$release = new Releases();
|
|
$release->delete($r['ID']);
|
|
printf("\n%s WARNING - Fixed broken release: %s ...\n",
|
|
date("Y-m-d H:i:s"), $r['name']);
|
|
// Free the variable in an attempt to recover memory
|
|
unset($release);
|
|
}else{
|
|
printf("\n%s WARNING - Missing NZB File: %d/%s ...\n",
|
|
date("Y-m-d H:i:s"), intval($r["ID"]), $r["name"]);
|
|
$offset++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Increment Process Count
|
|
$processed += 1;
|
|
|
|
$gid = $sn->nzbToGID($nzbfile);
|
|
// Free the variable in an attempt to recover memory
|
|
unset($nzbfile);
|
|
|
|
if(!$gid){
|
|
if($delete_broken_releases){
|
|
$release = new Releases();
|
|
$release->delete($r['ID']);
|
|
printf("\n%s WARNING - Fixed broken release: %s ...\n",
|
|
date("Y-m-d H:i:s"), $r['name']);
|
|
// Free the variable in an attempt to recover memory
|
|
unset($release);
|
|
}else{
|
|
printf("\n%s WARNING - Failed to retrieve GID for: %s ...\n",
|
|
date("Y-m-d H:i:s"), $r['name']);
|
|
$offset++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Update DB With Global Identifer
|
|
$ures = $db->query(sprintf($usql, $gid, $r['ID']));
|
|
if($db->getAffectedRows() < 0){
|
|
printf("\n%s ERROR - Failed to update: %s ...\n",
|
|
date("Y-m-d H:i:s"), $r['name']);
|
|
}
|
|
|
|
// make noise...
|
|
echo '.';
|
|
}
|
|
}
|
|
if($processed)echo "\n";
|
|
printf("%s INFO - Processed: %d record(s)\n",
|
|
date("Y-m-d H:i:s"), $processed);
|
|
|
|
printf("%s INFO - Updating GID in releasecomments table ...", date("Y-m-d H:i:s"));
|
|
|
|
# Batch update for comment table
|
|
$usql = 'UPDATE releasecomment, releases '
|
|
.'SET releasecomment.GID = releases.GID '
|
|
.'WHERE releases.ID = releasecomment.releaseID '
|
|
.'AND releasecomment.GID IS NULL '
|
|
.'AND releases.GID IS NOT NULL ';
|
|
|
|
// Reset Processed Count
|
|
$processed = 0;
|
|
$res = $db->query(sprintf($usql));
|
|
$affected = $db->getAffectedRows();
|
|
if($affected >= 0)
|
|
$processed += $affected;
|
|
echo " Done\n";
|
|
|
|
printf("%s INFO - Processed: %d comments(s)\n",
|
|
date("Y-m-d H:i:s"), $processed);
|
|
}
|
|
|
|
$force_keygen_save = false;
|
|
if(array_key_exists("K", $options) ||
|
|
array_key_exists("force-keygen", $options)){
|
|
// Enable flag to force keygen to save always
|
|
$force_keygen_save = true;
|
|
// Ensure keygen is ran
|
|
$options['keygen'] = true;
|
|
}
|
|
|
|
if(array_key_exists("k", $options) ||
|
|
array_key_exists("keygen", $options)){
|
|
$do_keygen = true;
|
|
$db = new DB();
|
|
$sn = new SpotNab();
|
|
|
|
$sql_prv = "SELECT value from site ".
|
|
"WHERE setting = 'spotnabsiteprvkey'";
|
|
$sql_pub = "SELECT value from site ".
|
|
"WHERE setting = 'spotnabsitepubkey'";
|
|
|
|
if($force_keygen_save === false){
|
|
$do_keygen = false;
|
|
// Check to see if keys are already generated first
|
|
// before continuing.
|
|
$res_prv = $db->queryOneRow($sql_prv);
|
|
$res_pub = $db->queryOneRow($sql_pub);
|
|
|
|
if($res_pub && $res_prv){
|
|
|
|
$prvkey = $sn->decompstr($res_prv['value']);
|
|
$pubkey = $sn->decompstr($res_pub['value']);
|
|
|
|
$str_in = $sn->getRandomStr(80);
|
|
$str_out = $sn->decrypt($sn->encrypt($str_in, $prvkey), $pubkey);
|
|
|
|
if($str_in != $str_out){
|
|
$do_keygen = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if($do_keygen)
|
|
{
|
|
printf("%s INFO - Generating new SSL Keys\n",
|
|
date("Y-m-d H:i:s"));
|
|
$keys = $sn->keygen();
|
|
if(is_array($keys)){
|
|
// Save Keys
|
|
$sql = sprintf("UPDATE site SET value = %s ".
|
|
"WHERE setting = 'spotnabsitepubkey'",
|
|
$db->escapeString($keys['pubkey']));
|
|
$db->query($sql);
|
|
printf("%s INFO - Saved new public key.\n",
|
|
date("Y-m-d H:i:s"));
|
|
//echo $keys['pubkey']."\n";
|
|
|
|
$sql = sprintf("UPDATE site SET value = %s ".
|
|
"WHERE setting = 'spotnabsiteprvkey'",
|
|
$db->escapeString($keys['prvkey']));
|
|
$db->query($sql);
|
|
printf("%s INFO - Saved new private key.\n",
|
|
date("Y-m-d H:i:s"));
|
|
}
|
|
}
|
|
|
|
$res_pub = $db->queryOneRow($sql_pub);
|
|
if($res_pub)
|
|
printf("SPOTNAB PUBLIC KEY (Begin copy from next line):\n%s\n", $res_pub['value']);
|
|
}
|
|
|
|
if(array_key_exists("p", $options) ||
|
|
array_key_exists("spotnab-post", $options)){
|
|
$sn = new SpotNab();
|
|
$sn->post();
|
|
}
|
|
|
|
if(array_key_exists("f", $options) ||
|
|
array_key_exists("spotnab-fetch", $options)){
|
|
$sn = new SpotNab();
|
|
$sn->fetch();
|
|
}
|