mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-09-01 18:58:56 +00:00
1421 lines
43 KiB
PHP
1421 lines
43 KiB
PHP
<?php
|
|
|
|
use newznab\db\Settings;
|
|
|
|
/**
|
|
* This class manages the downloading of binaries and parts from usenet, and the
|
|
* managing of data in the binaries and parts tables.
|
|
*
|
|
*/
|
|
class Binaries
|
|
{
|
|
const OPT_BLACKLIST = 1;
|
|
const OPT_WHITELIST = 2;
|
|
|
|
const BLACKLIST_DISABLED = 0;
|
|
const BLACKLIST_ENABLED = 1;
|
|
|
|
const BLACKLIST_FIELD_SUBJECT = 1;
|
|
const BLACKLIST_FIELD_FROM = 2;
|
|
const BLACKLIST_FIELD_MESSAGEID = 3;
|
|
|
|
/**
|
|
* How many headers do we download per loop?
|
|
*
|
|
* @var int
|
|
*/
|
|
public $messageBuffer;
|
|
|
|
/**
|
|
* @var ColorCLI
|
|
*/
|
|
protected $_colorCLI;
|
|
|
|
/**
|
|
* @var Logger
|
|
*/
|
|
protected $_debugging;
|
|
|
|
/**
|
|
* @var Groups
|
|
*/
|
|
protected $_groups;
|
|
|
|
/**
|
|
* @var NNTP
|
|
*/
|
|
protected $_nntp;
|
|
|
|
/**
|
|
* Should we use part repair?
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $_partRepair;
|
|
|
|
/**
|
|
* @var newznab\db\Settings
|
|
*/
|
|
protected $_pdo;
|
|
|
|
/**
|
|
* How many days to go back on a new group?
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $_newGroupScanByDays;
|
|
|
|
/**
|
|
* How many headers to download on new groups?
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $_newGroupMessagesToScan;
|
|
|
|
/**
|
|
* How many days to go back on new groups?
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $_newGroupDaysToScan;
|
|
|
|
/**
|
|
* How many headers to download per run of part repair?
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $_partRepairLimit;
|
|
|
|
/**
|
|
* Should we use table per group?
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $_tablePerGroup;
|
|
|
|
/**
|
|
* Echo to cli?
|
|
*
|
|
* @var bool
|
|
*/
|
|
protected $_echoCLI;
|
|
|
|
/**
|
|
* @var bool
|
|
*/
|
|
protected $_debug = false;
|
|
|
|
/**
|
|
* Max tries to download headers.
|
|
* @var int
|
|
*/
|
|
protected $_partRepairMaxTries;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param array $options Class instances / echo to CLI?
|
|
*/
|
|
public function __construct(array $options = [])
|
|
{
|
|
$defaults = [
|
|
'Echo' => true,
|
|
'ColorCLI' => null,
|
|
'Logger' => null,
|
|
'Groups' => null,
|
|
'NNTP' => null,
|
|
'Settings' => null,
|
|
];
|
|
$options += $defaults;
|
|
|
|
$this->_colorCLI = ($options['ColorCLI'] instanceof \ColorCLI ? $options['ColorCLI'] : new \ColorCLI());
|
|
$this->_echoCLI = ($options['Echo'] && NN_ECHOCLI);
|
|
$this->_pdo = ($options['Settings'] instanceof Settings ? $options['Settings'] : new Settings());
|
|
$this->_nntp = ($options['NNTP'] instanceof \NNTP ? $options['NNTP'] : new \NNTP(['Echo' => $this->_colorCLI, 'Settings' => $this->_pdo, 'ColorCLI' => $this->_colorCLI]));
|
|
$this->_groups = ($options['Groups'] instanceof \Groups ? $options['Groups'] : new \Groups(['Settings' => $this->_pdo]));
|
|
|
|
$this->_debug = (NN_DEBUG || NN_LOGGING);
|
|
if ($this->_debug) {
|
|
try {
|
|
$this->_debugging = new \Logger(['ColorCLI' => $this->_colorCLI]);
|
|
} catch (\LoggerException $error) {
|
|
$this->_debug = false;
|
|
}
|
|
}
|
|
|
|
$this->n = "\n";
|
|
|
|
$this->messageBuffer = ($this->_pdo->getSetting('maxmssgs') != '') ? $this->_pdo->getSetting('maxmssgs') : 20000;
|
|
$this->_compressedHeaders = ($this->_pdo->getSetting('compressedheaders') == "1") ? true : false;
|
|
$this->MaxMsgsPerRun = (!empty($this->_pdo->getSetting('maxmsgsperrun'))) ? $this->_pdo->getSetting('maxmsgsperrun') : 200000;
|
|
$this->_newGroupScanByDays = ($this->_pdo->getSetting('newsgroupscanmethod') == "1") ? true : false;
|
|
$this->_newGroupMessagesToScan = (!empty($this->_pdo->getSetting('newgroupmsgstoscan'))) ? $this->_pdo->getSetting('newgroupmsgstoscan') : 50000;
|
|
$this->_newGroupDaysToScan = (!empty($this->_pdo->getSetting('newgroupdaystoscan'))) ? $this->_pdo->getSetting('newgroupdaystoscan') : 3;
|
|
$this->_tablePerGroup = ($this->_pdo->getSetting('tablepergroup') == 1 ? true : false);
|
|
$this->_partRepair = ($this->_pdo->getSetting('partrepair') == 0 ? false : true);
|
|
$this->_partRepairLimit = ($this->_pdo->getSetting('maxpartrepair') != '') ? (int)$this->_pdo->getSetting('maxpartrepair') : 15000;
|
|
$this->_partRepairMaxTries = ($this->_pdo->getSetting('partrepairmaxtries') != '' ? (int)$this->_pdo->getSetting('partrepairmaxtries') : 3);
|
|
|
|
$this->blackList = array(); //cache of our black/white list
|
|
$this->blackList_by_group = array();
|
|
$this->message = array();
|
|
$this->startUpdate = microtime(true);
|
|
$this->startLoop = microtime(true);
|
|
$this->startHeaders = microtime(true);
|
|
|
|
$this->onlyProcessRegexBinaries = false;
|
|
}
|
|
|
|
/**
|
|
* Download new headers for all active groups.
|
|
*
|
|
* @param int $maxHeaders (Optional) How many headers to download max.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function updateAllGroups($maxHeaders = 0)
|
|
{
|
|
$groups = $this->_groups->getActive();
|
|
|
|
$groupCount = count($groups);
|
|
if ($groupCount > 0) {
|
|
$counter = 1;
|
|
$allTime = microtime(true);
|
|
|
|
$this->log(
|
|
'Updating: ' . $groupCount . ' group(s) - Using compression? ' . ($this->_compressedHeaders ? 'Yes' : 'No'),
|
|
'updateAllGroups',
|
|
\Logger::LOG_INFO,
|
|
'header'
|
|
);
|
|
|
|
// Loop through groups.
|
|
foreach ($groups as $group) {
|
|
$this->log(
|
|
'Starting group ' . $counter . ' of ' . $groupCount,
|
|
'updateAllGroups',
|
|
\Logger::LOG_INFO,
|
|
'header'
|
|
);
|
|
$this->updateGroup($group, $maxHeaders);
|
|
$counter++;
|
|
}
|
|
|
|
$this->log(
|
|
'Updating completed in ' . number_format(microtime(true) - $allTime, 2) . ' seconds.',
|
|
'updateAllGroups',
|
|
\Logger::LOG_INFO,
|
|
'primary'
|
|
);
|
|
} else {
|
|
$this->log(
|
|
'No groups specified. Ensure groups are added to newznab\'s database for updating.',
|
|
'updateAllGroups',
|
|
\Logger::LOG_NOTICE,
|
|
'warning'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Download new headers for a single group.
|
|
*
|
|
* @param array $groupMySQL Array of MySQL results for a single group.
|
|
* @param int $maxHeaders (Optional) How many headers to download max.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function updateGroup($groupMySQL, $maxHeaders = 0)
|
|
{
|
|
$startGroup = microtime(true);
|
|
|
|
// Select the group on the NNTP server, gets the latest info on it.
|
|
$groupNNTP = $this->_nntp->selectGroup($groupMySQL['name']);
|
|
if ($this->_nntp->isError($groupNNTP)) {
|
|
$groupNNTP = $this->_nntp->dataError($this->_nntp, $groupMySQL['name']);
|
|
if ($this->_nntp->isError($groupNNTP)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ($this->_echoCLI) {
|
|
$this->_colorCLI->doEcho($this->_colorCLI->primary('Processing ' . $groupMySQL['name']), true);
|
|
}
|
|
if ($groupMySQL['regexmatchonly'] == 1)
|
|
{
|
|
$this->onlyProcessRegexBinaries = true;
|
|
if ($this->_echoCLI) {
|
|
$this->_colorCLI->doEcho($this->_colorCLI->primary('Note: Discarding parts that do not match a regex', true));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$this->onlyProcessRegexBinaries = false;
|
|
}
|
|
|
|
// Attempt to repair any missing parts before grabbing new ones.
|
|
if ($groupMySQL['last_record'] != 0) {
|
|
if ($this->_partRepair) {
|
|
if ($this->_echoCLI) {
|
|
$this->_colorCLI->doEcho($this->_colorCLI->primary('Part repair enabled. Checking for missing parts.'), true);
|
|
}
|
|
$this->partRepair($groupMySQL);
|
|
} else if ($this->_echoCLI) {
|
|
$this->_colorCLI->doEcho($this->_colorCLI->primary('Part repair disabled by user.'), true);
|
|
}
|
|
}
|
|
|
|
// Generate postdate for first record, for those that upgraded.
|
|
if (is_null($groupMySQL['first_record_postdate']) && $groupMySQL['first_record'] != 0) {
|
|
|
|
$groupMySQL['first_record_postdate'] = $this->postdate($groupMySQL['first_record'], $groupNNTP);
|
|
|
|
$this->_pdo->queryExec(
|
|
sprintf('
|
|
UPDATE groups
|
|
SET first_record_postdate = %s
|
|
WHERE id = %d',
|
|
$this->_pdo->from_unixtime($groupMySQL['first_record_postdate']),
|
|
$groupMySQL['id']
|
|
)
|
|
);
|
|
}
|
|
|
|
// Get first article we want aka the oldest.
|
|
if ($groupMySQL['last_record'] == 0) {
|
|
if ($this->_newGroupScanByDays) {
|
|
// For new newsgroups - determine here how far we want to go back using date.
|
|
$first = $this->daytopost($this->_newGroupDaysToScan, $groupNNTP);
|
|
} else if ($groupNNTP['first'] >= ($groupNNTP['last'] - ($this->_newGroupMessagesToScan + $this->messageBuffer))) {
|
|
// If what we want is lower than the groups first article, set the wanted first to the first.
|
|
$first = $groupNNTP['first'];
|
|
} else {
|
|
// Or else, use the newest article minus how much we should get for new groups.
|
|
$first = (string)($groupNNTP['last'] - ($this->_newGroupMessagesToScan + $this->messageBuffer));
|
|
}
|
|
|
|
// We will use this to subtract so we leave articles for the next time (in case the server doesn't have them yet)
|
|
$leaveOver = $this->messageBuffer;
|
|
|
|
// If this is not a new group, go from our newest to the servers newest.
|
|
} else {
|
|
// Set our oldest wanted to our newest local article.
|
|
$first = $groupMySQL['last_record'];
|
|
|
|
// This is how many articles we will grab. (the servers newest minus our newest).
|
|
$totalCount = (string)($groupNNTP['last'] - $first);
|
|
|
|
// Check if the server has more articles than our loop limit x 2.
|
|
if ($totalCount > ($this->messageBuffer * 2)) {
|
|
// Get the remainder of $totalCount / $this->message buffer
|
|
$leaveOver = round(($totalCount % $this->messageBuffer), 0, PHP_ROUND_HALF_DOWN) + $this->messageBuffer;
|
|
} else {
|
|
// Else get half of the available.
|
|
$leaveOver = round(($totalCount / 2), 0, PHP_ROUND_HALF_DOWN);
|
|
}
|
|
}
|
|
|
|
// The last article we want, aka the newest.
|
|
$last = $groupLast = (string)($groupNNTP['last'] - $leaveOver);
|
|
|
|
// If the newest we want is older than the oldest we want somehow.. set them equal.
|
|
if ($last < $first) {
|
|
$last = $groupLast = $first;
|
|
}
|
|
|
|
// This is how many articles we are going to get.
|
|
$total = (string)($groupLast - $first);
|
|
// This is how many articles are available (without $leaveOver).
|
|
$realTotal = (string)($groupNNTP['last'] - $first);
|
|
|
|
// Check if we should limit the amount of fetched new headers.
|
|
if ($maxHeaders > 0) {
|
|
if ($maxHeaders < ($groupLast - $first)) {
|
|
$groupLast = $last = (string)($maxHeaders + $first);
|
|
}
|
|
$total = (string)($groupLast - $first);
|
|
}
|
|
|
|
// If total is bigger than 0 it means we have new parts in the newsgroup.
|
|
if ($total > 0) {
|
|
|
|
if ($this->_echoCLI) {
|
|
$this->_colorCLI->doEcho(
|
|
$this->_colorCLI->primary(
|
|
($groupMySQL['last_record'] == 0
|
|
? 'New group ' . $groupNNTP['group'] . ' starting with ' .
|
|
($this->_newGroupScanByDays
|
|
? $this->_newGroupDaysToScan . ' days'
|
|
: number_format($this->_newGroupMessagesToScan) . ' messages'
|
|
) . ' worth.'
|
|
: 'Group ' . $groupNNTP['group'] . ' has ' . number_format($realTotal) . ' new articles.'
|
|
) .
|
|
' Leaving ' . number_format($leaveOver) .
|
|
" for next pass.\nServer oldest: " . number_format($groupNNTP['first']) .
|
|
' Server newest: ' . number_format($groupNNTP['last']) .
|
|
' Local newest: ' . number_format($groupMySQL['last_record'])
|
|
), true
|
|
);
|
|
}
|
|
|
|
$done = false;
|
|
// Get all the parts (in portions of $this->messageBuffer to not use too much memory).
|
|
while ($done === false) {
|
|
|
|
// Increment last until we reach $groupLast (group newest article).
|
|
if ($total > $this->messageBuffer) {
|
|
$last = (string)($first + $this->messageBuffer) > $groupLast ? $groupLast : (string)($first + $this->messageBuffer);
|
|
}
|
|
// Increment first so we don't get an article we already had.
|
|
$first++;
|
|
|
|
if ($this->_echoCLI) {
|
|
$this->_colorCLI->doEcho(
|
|
$this->_colorCLI->header(
|
|
"\nGetting " . number_format($last - $first + 1) . ' articles (' . number_format($first) .
|
|
' to ' . number_format($last) . ') from ' . $groupMySQL['name'] . " - (" .
|
|
number_format($groupLast - $last) . " articles in queue)."
|
|
)
|
|
);
|
|
}
|
|
|
|
// Get article headers from newsgroup.
|
|
$scanSummary = $this->scan($groupMySQL, $first, $last);
|
|
|
|
// Check if we fetched headers.
|
|
if (!empty($scanSummary)) {
|
|
|
|
// If new group, update first record & postdate
|
|
if (is_null($groupMySQL['first_record_postdate']) && $groupMySQL['first_record'] == 0) {
|
|
$groupMySQL['first_record'] = $scanSummary['firstArticleNumber'];
|
|
|
|
if (isset($scanSummary['firstArticleDate'])) {
|
|
$groupMySQL['first_record_postdate'] = strtotime($scanSummary['firstArticleDate']);
|
|
} else {
|
|
$groupMySQL['first_record_postdate'] = $this->postdate($groupMySQL['first_record'], $groupNNTP);
|
|
}
|
|
|
|
$this->_pdo->queryExec(
|
|
sprintf('
|
|
UPDATE groups
|
|
SET first_record = %s, first_record_postdate = %s
|
|
WHERE id = %d',
|
|
$scanSummary['firstArticleNumber'],
|
|
$this->_pdo->from_unixtime($this->_pdo->escapeString($groupMySQL['first_record_postdate'])),
|
|
$groupMySQL['id']
|
|
)
|
|
);
|
|
}
|
|
|
|
if (isset($scanSummary['lastArticleDate'])) {
|
|
$scanSummary['lastArticleDate'] = strtotime($scanSummary['lastArticleDate']);
|
|
} else {
|
|
$scanSummary['lastArticleDate'] = $this->postdate($scanSummary['lastArticleNumber'], $groupNNTP);
|
|
}
|
|
|
|
$this->_pdo->queryExec(
|
|
sprintf('
|
|
UPDATE groups
|
|
SET last_record = %s, last_record_postdate = %s, last_updated = NOW()
|
|
WHERE id = %d',
|
|
$this->_pdo->escapeString($scanSummary['lastArticleNumber']),
|
|
$this->_pdo->from_unixtime($scanSummary['lastArticleDate']),
|
|
$groupMySQL['id']
|
|
)
|
|
);
|
|
} else {
|
|
// If we didn't fetch headers, update the record still.
|
|
$this->_pdo->queryExec(
|
|
sprintf('
|
|
UPDATE groups
|
|
SET last_record = %s, last_updated = NOW()
|
|
WHERE id = %d',
|
|
$this->_pdo->escapeString($last),
|
|
$groupMySQL['id']
|
|
)
|
|
);
|
|
}
|
|
|
|
if ($last == $groupLast) {
|
|
$done = true;
|
|
} else {
|
|
$first = $last;
|
|
}
|
|
}
|
|
|
|
if ($this->_echoCLI) {
|
|
$this->_colorCLI->doEcho(
|
|
$this->_colorCLI->primary(
|
|
PHP_EOL . 'Group ' . $groupMySQL['name'] . ' processed in ' .
|
|
number_format(microtime(true) - $startGroup, 2) . ' seconds.'
|
|
), true
|
|
);
|
|
}
|
|
} else if ($this->_echoCLI) {
|
|
$this->_colorCLI->doEcho(
|
|
$this->_colorCLI->primary(
|
|
'No new articles for ' . $groupMySQL['name'] . ' (first ' . number_format($first) .
|
|
', last ' . number_format($last) . ', grouplast ' . number_format($groupMySQL['last_record']) .
|
|
', total ' . number_format($total) . ")\n" . 'Server oldest: ' . number_format($groupNNTP['first']) .
|
|
' Server newest: ' . number_format($groupNNTP['last']) . ' Local newest: ' . number_format($groupMySQL['last_record'])
|
|
), true
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Download a range of usenet messages. Store binaries with subjects matching a
|
|
* specific pattern in the database.
|
|
*
|
|
* @param $groupArr
|
|
* @param $first
|
|
* @param $last
|
|
* @param string $type
|
|
*
|
|
* @return array
|
|
*/
|
|
function scan($groupArr, $first, $last, $type = 'update')
|
|
{
|
|
$db = new Settings();
|
|
$releaseRegex = new ReleaseRegex;
|
|
$n = $this->n;
|
|
// Check if MySQL tables exist, create if they do not, get their names at the same time.
|
|
$tableNames = $this->_groups->getCBPTableNames($this->_tablePerGroup, $groupArr['id']);
|
|
$partRepair = ($type === 'partrepair');
|
|
$returnArray = [];
|
|
|
|
// Download the headers.
|
|
if ($partRepair === true) {
|
|
// This is slower but possibly is better with missing headers.
|
|
$msgs = $this->_nntp->getOverview($first . '-' . $last, true, false);
|
|
} else {
|
|
$msgs = $this->_nntp->getXOVER($first . '-' . $last);
|
|
}
|
|
|
|
// If there was an error, try to reconnect.
|
|
if ($this->_nntp->isError($msgs)) {
|
|
|
|
// Increment if part repair and return false.
|
|
if ($partRepair === true) {
|
|
$this->_pdo->queryExec(
|
|
sprintf(
|
|
'UPDATE %s SET attempts = attempts + 1 WHERE groupid = %d AND numberid %s',
|
|
$tableNames['prname'],
|
|
$groupArr['id'],
|
|
($first == $last ? '= ' . $first : 'IN (' . implode(',', range($first, $last)) . ')')
|
|
)
|
|
);
|
|
return $returnArray;
|
|
}
|
|
|
|
// This is usually a compression error, so try disabling compression.
|
|
$this->_nntp->doQuit();
|
|
if ($this->_nntp->doConnect(false) !== true) {
|
|
return $returnArray;
|
|
}
|
|
|
|
// Re-select group, download headers again without compression and re-enable compression.
|
|
$this->_nntp->selectGroup($groupArr['name']);
|
|
$msgs = $this->_nntp->getXOVER($first . '-' . $last);
|
|
$this->_nntp->enableCompression();
|
|
|
|
// Check if the non-compression headers have an error.
|
|
if ($this->_nntp->isError($msgs)) {
|
|
$this->log(
|
|
"Code {$msgs->code}: {$msgs->message}\nSkipping group: {$groupArr['name']}",
|
|
'scan',
|
|
\Logger::LOG_WARNING,
|
|
'error'
|
|
);
|
|
return $returnArray;
|
|
}
|
|
}
|
|
|
|
$rangerequested = range($first, $last);
|
|
$msgsreceived = array();
|
|
$msgsblacklisted = array();
|
|
$msgsignored = array();
|
|
$msgsinserted = array();
|
|
$msgsnotinserted = array();
|
|
|
|
$timeHeaders = number_format(microtime(true) - $this->startHeaders, 2);
|
|
|
|
if ($this->_nntp->isError($msgs)) {
|
|
echo "Error {$msgs->code}: {$msgs->message}$n";
|
|
echo "Skipping group$n";
|
|
|
|
return $returnArray;
|
|
}
|
|
|
|
// Check if we got headers.
|
|
$msgCount = count($msgs);
|
|
|
|
if ($msgCount < 1) {
|
|
return $returnArray;
|
|
}
|
|
|
|
// Get highest and lowest article numbers/dates.
|
|
$iterator1 = 0;
|
|
$iterator2 = $msgCount - 1;
|
|
while (true) {
|
|
if (!isset($returnArray['firstArticleNumber']) && isset($msgs[$iterator1]['Number'])) {
|
|
$returnArray['firstArticleNumber'] = $msgs[$iterator1]['Number'];
|
|
$returnArray['firstArticleDate'] = $msgs[$iterator1]['Date'];
|
|
}
|
|
|
|
if (!isset($returnArray['lastArticleNumber']) && isset($msgs[$iterator2]['Number'])) {
|
|
$returnArray['lastArticleNumber'] = $msgs[$iterator2]['Number'];
|
|
$returnArray['lastArticleDate'] = $msgs[$iterator2]['Date'];
|
|
}
|
|
|
|
// Break if we found non empty articles.
|
|
if (isset($returnArray['firstArticleNumber']) && isset($returnArray['lastArticleNumber'])) {
|
|
break;
|
|
}
|
|
|
|
// Break out if we couldn't find anything.
|
|
if ($iterator1++ >= $msgCount - 1 || $iterator2-- <= 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (is_array($msgs)) {
|
|
//loop headers, figure out parts
|
|
foreach ($msgs AS $msg) {
|
|
if (!isset($msg['Number']))
|
|
continue;
|
|
|
|
$msgsreceived[] = $msg['Number'];
|
|
$msgPart = $msgTotalParts = 0;
|
|
|
|
$pattern = '|\((\d+)[\/](\d+)\)|i';
|
|
preg_match_all($pattern, $msg['Subject'], $matches, PREG_PATTERN_ORDER);
|
|
$matchcnt = sizeof($matches[0]);
|
|
for ($i = 0; $i < $matchcnt; $i++) {
|
|
$msgPart = $matches[1][$i];
|
|
$msgTotalParts = $matches[2][$i];
|
|
}
|
|
|
|
if (!isset($msg['Subject']) || $matchcnt == 0) // not a binary post most likely.. continue
|
|
{
|
|
$msgsignored[] = $msg['Number'];
|
|
continue;
|
|
}
|
|
|
|
if ((int)$msgPart > 0 && (int)$msgTotalParts > 0) {
|
|
$subject = utf8_encode(trim(preg_replace('|\(' . $msgPart . '[\/]' . $msgTotalParts . '\)|i', '', $msg['Subject'])));
|
|
|
|
if (!isset($this->message[$subject])) {
|
|
$this->message[$subject] = $msg;
|
|
$this->message[$subject]['MaxParts'] = (int)$msgTotalParts;
|
|
$this->message[$subject]['Date'] = strtotime($this->message[$subject]['Date']);
|
|
}
|
|
if ((int)$msgPart > 0) {
|
|
$this->message[$subject]['Parts'][(int)$msgPart] = array('Message-ID' => substr($msg['Message-ID'], 1, -1), 'number' => $msg['Number'], 'part' => (int)$msgPart, 'size' => $msg['Bytes']);
|
|
$this->message[$subject]['PartNumbers'][(int)$msgPart] = $msg['Number'];
|
|
}
|
|
}
|
|
}
|
|
unset($msg);
|
|
unset($msgs);
|
|
$count = 0;
|
|
$updatecount = 0;
|
|
$partcount = 0;
|
|
$rangenotreceived = array_diff($rangerequested, $msgsreceived);
|
|
|
|
if ($type != 'partrepair')
|
|
echo "Received " . sizeof($msgsreceived) . " articles of " . ($last - $first + 1) . " requested, " . sizeof($msgsignored) . " not binaries $n";
|
|
|
|
if ($type == 'update' && sizeof($msgsreceived) == 0) {
|
|
echo "Error: Server did not return any articles.$n";
|
|
echo "Skipping group$n";
|
|
|
|
return $returnArray;
|
|
}
|
|
|
|
if (sizeof($rangenotreceived) > 0) {
|
|
switch ($type) {
|
|
case 'backfill':
|
|
//don't add missing articles
|
|
break;
|
|
case 'partrepair':
|
|
case 'update':
|
|
default:
|
|
$this->addMissingParts($rangenotreceived, $tableNames['prname'], $groupArr['id']);
|
|
break;
|
|
}
|
|
echo "Server did not return " . count($rangenotreceived) . " article(s).$n";
|
|
}
|
|
|
|
if (isset($this->message) && count($this->message)) {
|
|
$groupRegexes = $releaseRegex->getForGroup($groupArr['name']);
|
|
|
|
//insert binaries and parts into database. when binary already exists; only insert new parts
|
|
foreach ($this->message AS $subject => $data) {
|
|
//Filter binaries based on black/white list
|
|
if ($this->isBlackListed($data, $groupArr['name'])) {
|
|
$msgsblacklisted[] = count($data['Parts']);
|
|
if ($type == 'partrepair') {
|
|
$partIds = array();
|
|
foreach ($data['Parts'] as $partdata)
|
|
$partIds[] = $partdata['number'];
|
|
$db->queryExec(sprintf("DELETE FROM %s WHERE numberid IN (%s) AND groupid=%d", $tableNames['prname'], implode(',', $partIds), $groupArr['id']));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (isset($data['Parts']) && count($data['Parts']) > 0 && $subject != '') {
|
|
//Check for existing binary
|
|
$binaryID = 0;
|
|
$binaryHash = md5($subject . $data['From'] . $groupArr['id']);
|
|
$res = $db->queryOneRow(sprintf("SELECT id FROM %s WHERE binaryhash = %s", $tableNames['bname'], $db->escapeString($binaryHash)));
|
|
if (!$res) {
|
|
|
|
//Apply Regexes
|
|
$regexMatches = array();
|
|
foreach ($groupRegexes as $groupRegex) {
|
|
$regexCheck = $releaseRegex->performMatch($groupRegex, $subject);
|
|
if ($regexCheck !== false) {
|
|
$regexMatches = $regexCheck;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$sql = '';
|
|
if (!empty($regexMatches)) {
|
|
$relparts = explode("/", $regexMatches['parts']);
|
|
$sql = sprintf('INSERT INTO %s (name, fromname, date, xref, totalparts, groupid, procstat, categoryid, regexid, reqid, relpart, reltotalpart, binaryhash, relname, dateadded) VALUES (%s, %s, FROM_UNIXTIME(%s), %s, %s, %d, %d, %s, %d, %s, %d, %d, %s, %s, now())', $tableNames['bname'], $db->escapeString($subject), $db->escapeString(utf8_encode($data['From'])), $db->escapeString($data['Date']), $db->escapeString($data['Xref']), $db->escapeString($data['MaxParts']), $groupArr['id'], Releases::PROCSTAT_TITLEMATCHED, $regexMatches['regcatid'], $regexMatches['regexid'], $db->escapeString($regexMatches['reqid']), $relparts[0], $relparts[1], $db->escapeString($binaryHash), $db->escapeString(str_replace('_', ' ', $regexMatches['name'])));
|
|
} elseif ($this->onlyProcessRegexBinaries === false) {
|
|
$sql = sprintf('INSERT INTO %s (name, fromname, date, xref, totalparts, groupid, binaryhash, dateadded) VALUES (%s, %s, FROM_UNIXTIME(%s), %s, %s, %d, %s, now())', $tableNames['bname'], $db->escapeString($subject), $db->escapeString(utf8_encode($data['From'])), $db->escapeString($data['Date']), $db->escapeString($data['Xref']), $db->escapeString($data['MaxParts']), $groupArr['id'], $db->escapeString($binaryHash));
|
|
} //onlyProcessRegexBinaries is true, there was no regex match and we are doing part repair so delete them
|
|
elseif ($type == 'partrepair') {
|
|
$partIds = array();
|
|
foreach ($data['Parts'] as $partdata)
|
|
$partIds[] = $partdata['number'];
|
|
$db->queryExec(sprintf('DELETE FROM %s WHERE numberid IN (%s) AND groupid = %d', $tableNames['prname'], implode(',', $partIds), $groupArr['id']));
|
|
continue;
|
|
}
|
|
if ($sql != '') {
|
|
$binaryID = $db->queryInsert($sql);
|
|
$count++;
|
|
//if ($count % 500 == 0) echo "$count bin adds...";
|
|
}
|
|
} else {
|
|
$binaryID = $res["id"];
|
|
$updatecount++;
|
|
//if ($updatecount % 500 == 0) echo "$updatecount bin updates...";
|
|
}
|
|
|
|
if ($binaryID != 0) {
|
|
$partParams = array();
|
|
$partNumbers = array();
|
|
foreach ($data['Parts'] AS $partdata) {
|
|
$partcount++;
|
|
|
|
$partParams[] = sprintf('(%d, %s, %s, %s, %s)', $binaryID, $db->escapeString($partdata['Message-ID']), $db->escapeString($partdata['number']), $db->escapeString(round($partdata['part'])), $db->escapeString($partdata['size']));
|
|
$partNumbers[] = $partdata['number'];
|
|
}
|
|
|
|
$partSql = ('INSERT INTO ' . $tableNames['pname'] . ' (binaryid, messageid, number, partnumber, size) VALUES '.implode(', ', $partParams));
|
|
$pidata = $db->queryInsert($partSql);
|
|
if (!$pidata) {
|
|
$msgsnotinserted = array_merge($msgsnotinserted, $partNumbers);
|
|
} else {
|
|
$msgsinserted = array_merge($msgsinserted, $partNumbers);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//TODO: determine whether to add to missing articles if insert failed
|
|
if (sizeof($msgsnotinserted) > 0) {
|
|
echo 'WARNING: ' . count($msgsnotinserted) . ' Parts failed to insert' . $n;
|
|
$this->addMissingParts($msgsnotinserted, $tableNames['prname'], $groupArr['id']);
|
|
}
|
|
if (($count >= 500) || ($updatecount >= 500)) {
|
|
echo $n;
|
|
} //line break for bin adds output
|
|
}
|
|
|
|
$timeUpdate = number_format(microtime(true) - $this->startUpdate, 2);
|
|
$timeLoop = number_format(microtime(true) - $this->startLoop, 2);
|
|
|
|
if (sizeof($msgsblacklisted) > 0)
|
|
echo "Blacklisted " . array_sum($msgsblacklisted) . " parts in " . sizeof($msgsblacklisted) . " binaries" . $n;
|
|
|
|
if ($type != 'partrepair') {
|
|
if ($this->_echoCLI) {
|
|
$this->_colorCLI->doEcho(
|
|
$this->_colorCLI->alternateOver(number_format($count)).
|
|
$this->_colorCLI->primaryOver(' new, ') .
|
|
$this->_colorCLI->alternateOver(number_format($updatecount)).
|
|
$this->_colorCLI->primaryOver(' updated, ') .
|
|
$this->_colorCLI->alternateOver(number_format($partcount)).
|
|
$this->_colorCLI->primaryOver(' parts, ') .
|
|
$this->_colorCLI->alternateOver($timeHeaders . 's') .
|
|
$this->_colorCLI->primaryOver(' to download articles, ') .
|
|
$this->_colorCLI->alternateOver($timeUpdate . 's') .
|
|
$this->_colorCLI->primaryOver(' to insert binaries/parts, ') .
|
|
$this->_colorCLI->alternateOver($timeLoop . 's') .
|
|
$this->_colorCLI->primary(' total.')
|
|
);
|
|
}
|
|
}
|
|
unset($this->message);
|
|
unset($data);
|
|
|
|
return $returnArray;
|
|
} else {
|
|
echo "Error: Can't get parts from server (msgs not array) $n";
|
|
echo "Skipping group$n";
|
|
|
|
return $returnArray;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempt to get missing article headers.
|
|
*
|
|
* @param array $groupArr The info for this group from mysql.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function partRepair($groupArr)
|
|
{
|
|
$tableNames = $this->_groups->getCBPTableNames($this->_tablePerGroup, $groupArr['id']);
|
|
// Get all parts in partrepair table.
|
|
$missingParts = $this->_pdo->query(
|
|
sprintf('
|
|
SELECT * FROM %s
|
|
WHERE groupid = %d AND attempts < %d
|
|
ORDER BY numberid ASC LIMIT %d',
|
|
$tableNames['prname'],
|
|
$groupArr['id'],
|
|
$this->_partRepairMaxTries,
|
|
$this->_partRepairLimit
|
|
)
|
|
);
|
|
|
|
$missingCount = count($missingParts);
|
|
if ($missingCount > 0) {
|
|
if ($this->_echoCLI) {
|
|
$this->_colorCLI->doEcho(
|
|
$this->_colorCLI->primary(
|
|
'Attempting to repair ' .
|
|
number_format($missingCount) .
|
|
' parts.'
|
|
), true
|
|
);
|
|
}
|
|
|
|
// Loop through each part to group into continuous ranges with a maximum range of messagebuffer/4.
|
|
$ranges = $partList = [];
|
|
$firstPart = $lastNum = $missingParts[0]['numberid'];
|
|
|
|
foreach ($missingParts as $part) {
|
|
if (($part['numberid'] - $firstPart) > ($this->messageBuffer / 4)) {
|
|
|
|
$ranges[] = [
|
|
'partfrom' => $firstPart,
|
|
'partto' => $lastNum,
|
|
'partlist' => $partList
|
|
];
|
|
|
|
$firstPart = $part['numberid'];
|
|
$partList = [];
|
|
}
|
|
$partList[] = $part['numberid'];
|
|
$lastNum = $part['numberid'];
|
|
}
|
|
|
|
$ranges[] = [
|
|
'partfrom' => $firstPart,
|
|
'partto' => $lastNum,
|
|
'partlist' => $partList
|
|
];
|
|
|
|
// Download missing parts in ranges.
|
|
foreach ($ranges as $range) {
|
|
|
|
$partFrom = $range['partfrom'];
|
|
$partTo = $range['partto'];
|
|
$partList = $range['partlist'];
|
|
|
|
if ($this->_echoCLI) {
|
|
echo chr(rand(45,46)) . "\r";
|
|
}
|
|
|
|
// Get article headers from newsgroup.
|
|
$this->scan($groupArr, $partFrom, $partTo, 'partrepair', $partList);
|
|
}
|
|
|
|
// Calculate parts repaired
|
|
$result = $this->_pdo->queryOneRow(
|
|
sprintf('
|
|
SELECT COUNT(id) AS num
|
|
FROM %s
|
|
WHERE groupid = %d
|
|
AND numberid <= %d',
|
|
$tableNames['prname'],
|
|
$groupArr['id'],
|
|
$missingParts[$missingCount - 1]['numberid']
|
|
)
|
|
);
|
|
|
|
$partsRepaired = 0;
|
|
if ($result !== false) {
|
|
$partsRepaired = ($missingCount - $result['num']);
|
|
}
|
|
|
|
// Update attempts on remaining parts for active group
|
|
if (isset($missingParts[$missingCount - 1]['id'])) {
|
|
$this->_pdo->queryExec(
|
|
sprintf('
|
|
UPDATE %s
|
|
SET attempts = attempts + 1
|
|
WHERE groupid = %d
|
|
AND numberid <= %d',
|
|
$tableNames['prname'],
|
|
$groupArr['id'],
|
|
$missingParts[$missingCount - 1]['numberid']
|
|
)
|
|
);
|
|
}
|
|
|
|
if ($this->_echoCLI) {
|
|
$this->_colorCLI->doEcho(
|
|
$this->_colorCLI->primary(
|
|
PHP_EOL .
|
|
number_format($partsRepaired) .
|
|
' parts repaired.'
|
|
), true
|
|
);
|
|
}
|
|
}
|
|
|
|
// Remove articles that we cant fetch after x attempts.
|
|
$this->_pdo->queryExec(
|
|
sprintf(
|
|
'DELETE FROM %s WHERE attempts >= %d AND groupid = %d',
|
|
$tableNames['prname'],
|
|
$this->_partRepairMaxTries,
|
|
$groupArr['id']
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Insert a missing part to the database.
|
|
*/
|
|
private function addMissingParts($numbers, $tablename, $groupID)
|
|
{
|
|
$db = new Settings();
|
|
$added = false;
|
|
$insertStr = "INSERT INTO $tablename (numberid, groupid) VALUES ";
|
|
foreach ($numbers as $number) {
|
|
if ($number > 0) {
|
|
$checksql = sprintf("select numberid from $tablename where numberid = %u and groupid = %d", $number, $groupID);
|
|
$chkrow = $db->queryOneRow($checksql);
|
|
if ($chkrow) {
|
|
$updsql = sprintf('update ' . $tablename . ' set attempts = attempts + 1 where numberid = %u and groupid = %d', $number, $groupID);
|
|
$db->queryExec($updsql);
|
|
} else {
|
|
$added = true;
|
|
$insertStr .= sprintf("(%u, %d), ", $number, $groupID);
|
|
}
|
|
}
|
|
}
|
|
if ($added) {
|
|
$insertStr = substr($insertStr, 0, -2);
|
|
|
|
return $db->queryInsert($insertStr, false);
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Return internally cached list of binary blacklist patterns.
|
|
*/
|
|
public function retrieveBlackList()
|
|
{
|
|
if (is_array($this->blackList) && !empty($this->blackList)) {
|
|
return $this->blackList;
|
|
}
|
|
$blackList = $this->getBlacklist(true);
|
|
$this->blackList = $blackList;
|
|
|
|
return $blackList;
|
|
}
|
|
|
|
/**
|
|
* Test if a message subject is blacklisted.
|
|
*/
|
|
public function isBlackListed($msg, $groupName)
|
|
{
|
|
if (empty($this->blackList_by_group[$groupName])) {
|
|
$main_blackList = $this->retrieveBlackList();
|
|
$this->blackList_by_group[$groupName] = array();
|
|
foreach ($main_blackList as $blist) {
|
|
if (preg_match('/^' . $blist['groupname'] . '$/i', $groupName)) {
|
|
$this->blackList_by_group[$groupName][] = $blist;
|
|
}
|
|
}
|
|
}
|
|
|
|
$blackList = $this->blackList_by_group[$groupName];
|
|
$field = array();
|
|
if (isset($msg["Subject"]))
|
|
$field[Binaries::BLACKLIST_FIELD_SUBJECT] = $msg["Subject"];
|
|
|
|
if (isset($msg["From"]))
|
|
$field[Binaries::BLACKLIST_FIELD_FROM] = $msg["From"];
|
|
|
|
if (isset($msg["Message-ID"]))
|
|
$field[Binaries::BLACKLIST_FIELD_MESSAGEID] = $msg["Message-ID"];
|
|
|
|
// if a white list is detected we now are required to
|
|
// only accept the entry if it matches at least 1 whitelist
|
|
// while a blacklist will over-ride all
|
|
$whitelist = array();
|
|
$matches_whitelist = false;
|
|
|
|
foreach ($blackList as $blist) {
|
|
//blacklist
|
|
if ($blist['optype'] == Binaries::OPT_BLACKLIST) {
|
|
if (preg_match('/' . $blist['regex'] . '/i', $field[$blist['msgcol']])) {
|
|
return true;
|
|
}
|
|
} else if ($blist['optype'] == Binaries::OPT_WHITELIST) {
|
|
$whitelist[] = $blist['regex'];
|
|
if (preg_match('/' . $blist['regex'] . '/i', $field[$blist['msgcol']])) {
|
|
// Flag that we matched the white list
|
|
$matches_whitelist = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
# We parsed entire matching list entries at this point.. now we need
|
|
# to handle the whitelist (if it was enabled)
|
|
if (count($whitelist) > 0 && !$matches_whitelist) {
|
|
# We failed to match white list
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Rawsearch. Perform a simple like match on binary subjects matching a pattern.
|
|
*/
|
|
public function search($search, $limit = 1000, $excludedcats = array())
|
|
{
|
|
$db = new Settings();
|
|
|
|
//
|
|
// if the query starts with a ^ it indicates the search is looking for items which start with the term
|
|
// still do the like match, but mandate that all items returned must start with the provided word
|
|
//
|
|
$words = explode(" ", $search);
|
|
$searchsql = "";
|
|
$intwordcount = 0;
|
|
if (count($words) > 0) {
|
|
foreach ($words as $word) {
|
|
//
|
|
// see if the first word had a caret, which indicates search must start with term
|
|
//
|
|
if ($intwordcount == 0 && (strpos($word, "^") === 0))
|
|
$searchsql .= sprintf(" and b.name like %s", $db->escapeString(substr($word, 1) . "%"));
|
|
else
|
|
$searchsql .= sprintf(" and b.name like %s", $db->escapeString("%" . $word . "%"));
|
|
|
|
$intwordcount++;
|
|
}
|
|
}
|
|
|
|
$exccatlist = "";
|
|
if (count($excludedcats) > 0)
|
|
$exccatlist = " and b.categoryid not in (" . implode(",", $excludedcats) . ") ";
|
|
|
|
$res = $db->query(sprintf("
|
|
SELECT b.*,
|
|
g.name AS group_name,
|
|
r.guid,
|
|
(SELECT COUNT(id) FROM parts p where p.binaryid = b.id) as 'binnum'
|
|
FROM binaries b
|
|
INNER JOIN groups g ON g.id = b.groupid
|
|
LEFT OUTER JOIN releases r ON r.id = b.releaseid
|
|
WHERE 1=1 %s %s order by DATE DESC LIMIT %d ",
|
|
$searchsql, $exccatlist, $limit
|
|
)
|
|
);
|
|
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* Get all binaries for a release.
|
|
*/
|
|
public function getForReleaseId($id)
|
|
{
|
|
$db = new Settings();
|
|
|
|
return $db->query(sprintf("select binaries.* from binaries where releaseid = %d order by relpart", $id));
|
|
}
|
|
|
|
/**
|
|
* Get a binary row.
|
|
*/
|
|
public function getById($id)
|
|
{
|
|
$db = new Settings();
|
|
|
|
return $db->queryOneRow(sprintf("select binaries.*, groups.name as groupname from binaries left outer join groups on binaries.groupid = groups.id where binaries.id = %d ", $id));
|
|
}
|
|
|
|
/**
|
|
* Get list of blacklists from database.
|
|
*/
|
|
public function getBlacklist($activeonly = true)
|
|
{
|
|
$db = new Settings();
|
|
|
|
$where = "";
|
|
if ($activeonly)
|
|
$where = " where binaryblacklist.status = 1 ";
|
|
|
|
return $db->query("SELECT binaryblacklist.id, binaryblacklist.optype, binaryblacklist.status, binaryblacklist.description, binaryblacklist.groupname AS groupname, binaryblacklist.regex,
|
|
groups.id AS groupid, binaryblacklist.msgcol FROM binaryblacklist
|
|
left outer JOIN groups ON groups.name = binaryblacklist.groupname
|
|
" . $where . "
|
|
ORDER BY coalesce(groupname,'zzz')"
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get a blacklist row from database.
|
|
*/
|
|
public function getBlacklistByID($id)
|
|
{
|
|
$db = new Settings();
|
|
|
|
return $db->queryOneRow(sprintf("select * from binaryblacklist where id = %d ", $id));
|
|
}
|
|
|
|
/**
|
|
* Delete a blacklist row from database.
|
|
*/
|
|
public function deleteBlacklist($id)
|
|
{
|
|
$db = new Settings();
|
|
|
|
return $db->queryExec(sprintf("DELETE from binaryblacklist where id = %d", $id));
|
|
}
|
|
|
|
/**
|
|
* Update a blacklist row.
|
|
*/
|
|
public function updateBlacklist($regex)
|
|
{
|
|
$db = new Settings();
|
|
|
|
$groupname = $regex["groupname"];
|
|
if ($groupname == "")
|
|
$groupname = "null";
|
|
else {
|
|
$groupname = preg_replace("/a\.b\./i", "alt.binaries.", $groupname);
|
|
$groupname = sprintf("%s", $db->escapeString($groupname));
|
|
}
|
|
|
|
$db->queryExec(sprintf("update binaryblacklist set groupname=%s, regex=%s, status=%d, description=%s, optype=%d, msgcol=%d where id = %d ", $groupname, $db->escapeString($regex["regex"]), $regex["status"], $db->escapeString($regex["description"]), $regex["optype"], $regex["msgcol"], $regex["id"]));
|
|
}
|
|
|
|
/**
|
|
* Add a new blacklist row.
|
|
*/
|
|
public function addBlacklist($regex)
|
|
{
|
|
$db = new Settings();
|
|
|
|
$groupname = $regex["groupname"];
|
|
if ($groupname == "")
|
|
$groupname = "null";
|
|
else {
|
|
$groupname = preg_replace("/a\.b\./i", "alt.binaries.", $groupname);
|
|
$groupname = sprintf("%s", $db->escapeString($groupname));
|
|
}
|
|
|
|
return $db->queryInsert(sprintf("insert into binaryblacklist (groupname, regex, status, description, optype, msgcol) values (%s, %s, %d, %s, %d, %d) ",
|
|
$groupname, $db->escapeString($regex["regex"]), $regex["status"], $db->escapeString($regex["description"]), $regex["optype"], $regex["msgcol"]
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Add a new binary row and its associated parts.
|
|
*/
|
|
public function delete($id)
|
|
{
|
|
$db = new Settings();
|
|
$db->queryExec(sprintf("DELETE from parts where binaryid = %d", $id));
|
|
$db->queryExec(sprintf("DELETE from binaries where id = %d", $id));
|
|
}
|
|
|
|
# http://php.net/manual/en/function.array-unique.php#97285
|
|
public function getSuperUniqueArray($array)
|
|
{
|
|
$result = array_map("unserialize", array_unique(array_map("serialize", $array)));
|
|
foreach ($result as $key => $value) {
|
|
if (is_array($value))
|
|
$result[$key] = $this->getSuperUniqueArray($value);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Returns article number based on # of days.
|
|
*
|
|
* @param int $days How many days back we want to go.
|
|
* @param array $data Group data from usenet.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function daytopost($days, $data)
|
|
{
|
|
$goalTime = time() - (86400 * $days);
|
|
// The time we want = current unix time (ex. 1395699114) - minus 86400 (seconds in a day)
|
|
// times days wanted. (ie 1395699114 - 2592000 (30days)) = 1393107114
|
|
|
|
// The servers oldest date.
|
|
$firstDate = $this->postdate($data['first'], $data);
|
|
if ($goalTime < $firstDate) {
|
|
// If the date we want is older than the oldest date in the group return the groups oldest article.
|
|
return $data['first'];
|
|
}
|
|
|
|
// The servers newest date.
|
|
$lastDate = $this->postdate($data['last'], $data);
|
|
if ($goalTime > $lastDate) {
|
|
// If the date we want is newer than the groups newest date, return the groups newest article.
|
|
return $data['last'];
|
|
}
|
|
|
|
if ($this->_echoCLI) {
|
|
$this->_colorCLI->doEcho(
|
|
$this->_colorCLI->primary(
|
|
'Searching for an approximate article number for group ' . $data['group'] . ' ' . $days . ' days back.'
|
|
)
|
|
);
|
|
}
|
|
|
|
// Pick the middle to start with
|
|
$wantedArticle = round(($data['last'] + $data['first']) / 2);
|
|
$aMax = $data['last'];
|
|
$aMin = $data['first'];
|
|
$reallyOldArticle = $oldArticle = $articleTime = null;
|
|
|
|
while(true) {
|
|
// Article exists outside of available range, this shouldn't happen
|
|
if ($wantedArticle <= $data['first'] || $wantedArticle >= $data['last']) {
|
|
break;
|
|
}
|
|
|
|
// Keep a note of the last articles we checked
|
|
$reallyOldArticle = $oldArticle;
|
|
$oldArticle = $wantedArticle;
|
|
|
|
// Get the date of this article
|
|
$articleTime = $this->postdate($wantedArticle, $data);
|
|
|
|
// Article doesn't exist, start again with something random
|
|
if (!$articleTime) {
|
|
$wantedArticle = mt_rand($aMin, $aMax);
|
|
$articleTime = $this->postdate($wantedArticle, $data);
|
|
}
|
|
|
|
if ($articleTime < $goalTime) {
|
|
// Article is older than we want
|
|
$aMin = $oldArticle;
|
|
$wantedArticle = round(($aMax + $oldArticle) / 2);
|
|
if ($this->_echoCLI) {
|
|
echo '-';
|
|
}
|
|
} else if ($articleTime > $goalTime) {
|
|
// Article is newer than we want
|
|
$aMax = $oldArticle;
|
|
$wantedArticle = round(($aMin + $oldArticle) / 2);
|
|
if ($this->_echoCLI) {
|
|
echo '+';
|
|
}
|
|
} else if ($articleTime == $goalTime) {
|
|
// Exact match. We did it! (this will likely never happen though)
|
|
break;
|
|
}
|
|
|
|
// We seem to be flip-flopping between 2 articles, assume we're out of articles to check.
|
|
// End on an article more recent than our oldest so that we don't miss any releases.
|
|
if ($reallyOldArticle == $wantedArticle && ($goalTime - $articleTime) <= 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
$wantedArticle = (int)$wantedArticle;
|
|
if ($this->_echoCLI) {
|
|
$this->_colorCLI->doEcho(
|
|
$this->_colorCLI->primary(
|
|
PHP_EOL . 'Found article #' . $wantedArticle . ' which has a date of ' . date('r', $articleTime) .
|
|
', vs wanted date of ' . date('r', $goalTime) . '. Difference from goal is ' . round(($goalTime - $articleTime) / 60 / 60 / 24, 1) . ' days.'
|
|
)
|
|
);
|
|
}
|
|
|
|
return $wantedArticle;
|
|
}
|
|
|
|
/**
|
|
* Convert unix time to days ago.
|
|
*
|
|
* @param int $timestamp unix time
|
|
*
|
|
* @return float
|
|
*/
|
|
private function daysOld($timestamp)
|
|
{
|
|
return round((time() - (!is_numeric($timestamp) ? strtotime($timestamp) : $timestamp)) / 86400, 1);
|
|
}
|
|
|
|
/**
|
|
* Returns unix time for an article number.
|
|
*
|
|
* @param int $post The article number to get the time from.
|
|
* @param array $groupData Usenet group info from NNTP selectGroup method.
|
|
*
|
|
* @return bool|int
|
|
*/
|
|
public function postdate($post, array $groupData)
|
|
{
|
|
// Set table names
|
|
$groupID = $this->_groups->getIDByName($groupData['group']);
|
|
$group = [];
|
|
if ($groupID !== '') {
|
|
$group = $this->_groups->getCBPTableNames($this->_tablePerGroup, $groupID);
|
|
}
|
|
|
|
$currentPost = $post;
|
|
|
|
$attempts = $date = 0;
|
|
do {
|
|
// Try to get the article date locally first.
|
|
if ($groupID !== '') {
|
|
// Try to get locally.
|
|
$local = $this->_pdo->queryOneRow(
|
|
sprintf('
|
|
SELECT b.date AS date
|
|
FROM %s b, %s p
|
|
WHERE b.id = p.binaryid
|
|
AND b.groupid = %s
|
|
AND p.number = %s LIMIT 1',
|
|
$group['bname'],
|
|
$group['pname'],
|
|
$groupID,
|
|
$currentPost
|
|
)
|
|
);
|
|
if ($local !== false) {
|
|
$date = $local['date'];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If we could not find it locally, try usenet.
|
|
$header = $this->_nntp->getXOVER($currentPost);
|
|
if (!$this->_nntp->isError($header)) {
|
|
// Check if the date is set.
|
|
if (isset($header[0]['Date']) && strlen($header[0]['Date']) > 0) {
|
|
$date = $header[0]['Date'];
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Try to get a different article number.
|
|
if (abs($currentPost - $groupData['first']) > abs($groupData['last'] - $currentPost)) {
|
|
$tempPost = round($currentPost / (mt_rand(1005, 1012) / 1000), 0, PHP_ROUND_HALF_UP);
|
|
if ($tempPost < $groupData['first']) {
|
|
$tempPost = $groupData['first'];
|
|
}
|
|
} else {
|
|
$tempPost = round((mt_rand(1005, 1012) / 1000) * $currentPost, 0, PHP_ROUND_HALF_UP);
|
|
if ($tempPost > $groupData['last']) {
|
|
$tempPost = $groupData['last'];
|
|
}
|
|
}
|
|
// If we got the same article number as last time, give up.
|
|
if ($tempPost === $currentPost) {
|
|
break;
|
|
}
|
|
$currentPost = $tempPost;
|
|
|
|
if ($this->_debug) {
|
|
$this->_colorCLI->doEcho($this->_colorCLI->debug('Postdate retried ' . $attempts . " time(s)."));
|
|
}
|
|
} while ($attempts++ <= 20);
|
|
|
|
// If we didn't get a date, set it to now.
|
|
if (!$date) {
|
|
$date = time();
|
|
} else {
|
|
$date = strtotime($date);
|
|
}
|
|
|
|
if ($this->_debug) {
|
|
$this->_debugging->log(
|
|
'Binaries',
|
|
"postdate",
|
|
'Article (' .
|
|
$post .
|
|
"'s) date is (" .
|
|
$date .
|
|
') (' .
|
|
$this->daysOld($date) .
|
|
" days old)",
|
|
\Logger::LOG_INFO
|
|
);
|
|
}
|
|
|
|
return $date;
|
|
}
|
|
/**
|
|
* Log / Echo message.
|
|
*
|
|
* @param string $message Message to log.
|
|
* @param string $method Method that called this.
|
|
* @param int $level Logger severity level constant.
|
|
* @param string $color ColorCLI method name.
|
|
*/
|
|
private function log($message, $method, $level, $color)
|
|
{
|
|
if ($this->_echoCLI) {
|
|
$this->_colorCLI->doEcho(
|
|
$this->_colorCLI->$color($message), true
|
|
);
|
|
}
|
|
|
|
if ($this->_debug) {
|
|
$this->_debugging->log('Binaries', $method, $message, $level);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete all Binaries/Parts for a group id.
|
|
*
|
|
* @param int $groupID The id of the group.
|
|
*
|
|
* @note A trigger automatically deletes the parts.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function purgeGroup($groupID)
|
|
{
|
|
$this->_pdo->queryExec(sprintf('DELETE b FROM binaries b WHERE b.groupid = %d', $groupID));
|
|
}
|
|
} |