| Total Complexity | 217 |
| Total Lines | 1726 |
| Duplicated Lines | 0 % |
| Changes | 0 | ||
Complex classes like Binaries often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Binaries, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 17 | class Binaries |
||
| 18 | { |
||
| 19 | public const OPTYPE_BLACKLIST = 1; |
||
| 20 | public const OPTYPE_WHITELIST = 2; |
||
| 21 | |||
| 22 | public const BLACKLIST_DISABLED = 0; |
||
| 23 | public const BLACKLIST_ENABLED = 1; |
||
| 24 | |||
| 25 | public const BLACKLIST_FIELD_SUBJECT = 1; |
||
| 26 | public const BLACKLIST_FIELD_FROM = 2; |
||
| 27 | public const BLACKLIST_FIELD_MESSAGEID = 3; |
||
| 28 | |||
| 29 | /** |
||
| 30 | * @var array |
||
| 31 | */ |
||
| 32 | public $blackList = []; |
||
| 33 | |||
| 34 | /** |
||
| 35 | * @var array |
||
| 36 | */ |
||
| 37 | public $whiteList = []; |
||
| 38 | |||
| 39 | /** |
||
| 40 | * @var int |
||
| 41 | */ |
||
| 42 | public $messageBuffer; |
||
| 43 | |||
| 44 | /** |
||
| 45 | * @var \Blacklight\ColorCLI |
||
| 46 | */ |
||
| 47 | protected $_colorCLI; |
||
| 48 | |||
| 49 | /** |
||
| 50 | * @var \Blacklight\CollectionsCleaning |
||
| 51 | */ |
||
| 52 | protected $_collectionsCleaning; |
||
| 53 | |||
| 54 | /** |
||
| 55 | * @var \Blacklight\NNTP |
||
| 56 | */ |
||
| 57 | protected $_nntp; |
||
| 58 | |||
| 59 | /** |
||
| 60 | * Should we use header compression? |
||
| 61 | * |
||
| 62 | * @var bool |
||
| 63 | */ |
||
| 64 | protected $_compressedHeaders; |
||
| 65 | |||
| 66 | /** |
||
| 67 | * Should we use part repair? |
||
| 68 | * |
||
| 69 | * @var bool |
||
| 70 | */ |
||
| 71 | protected $_partRepair; |
||
| 72 | |||
| 73 | /** |
||
| 74 | * @var \Blacklight\db\DB |
||
| 75 | */ |
||
| 76 | protected $_pdo; |
||
| 77 | |||
| 78 | /** |
||
| 79 | * How many days to go back on a new group? |
||
| 80 | * |
||
| 81 | * @var bool |
||
| 82 | */ |
||
| 83 | protected $_newGroupScanByDays; |
||
| 84 | |||
| 85 | /** |
||
| 86 | * How many headers to download on new groups? |
||
| 87 | * |
||
| 88 | * @var int |
||
| 89 | */ |
||
| 90 | protected $_newGroupMessagesToScan; |
||
| 91 | |||
| 92 | /** |
||
| 93 | * How many days to go back on new groups? |
||
| 94 | * |
||
| 95 | * @var int |
||
| 96 | */ |
||
| 97 | protected $_newGroupDaysToScan; |
||
| 98 | |||
| 99 | /** |
||
| 100 | * How many headers to download per run of part repair? |
||
| 101 | * |
||
| 102 | * @var int |
||
| 103 | */ |
||
| 104 | protected $_partRepairLimit; |
||
| 105 | |||
| 106 | /** |
||
| 107 | * Should we show dropped yEnc to CLI? |
||
| 108 | * |
||
| 109 | * @var bool |
||
| 110 | */ |
||
| 111 | protected $_showDroppedYEncParts; |
||
| 112 | |||
| 113 | /** |
||
| 114 | * Echo to cli? |
||
| 115 | * |
||
| 116 | * @var bool |
||
| 117 | */ |
||
| 118 | protected $_echoCLI; |
||
| 119 | |||
| 120 | /** |
||
| 121 | * @var bool |
||
| 122 | */ |
||
| 123 | protected $_debug = false; |
||
| 124 | |||
| 125 | /** |
||
| 126 | * Max tries to download headers. |
||
| 127 | * @var int |
||
| 128 | */ |
||
| 129 | protected $_partRepairMaxTries; |
||
| 130 | |||
| 131 | /** |
||
| 132 | * An array of binaryblacklist IDs that should have their activity date updated. |
||
| 133 | * @var array(int) |
||
| 134 | */ |
||
| 135 | protected $_binaryBlacklistIdsToUpdate = []; |
||
| 136 | |||
| 137 | /** |
||
| 138 | * @var float microseconds time of cleaning process start |
||
| 139 | */ |
||
| 140 | protected $startCleaning; |
||
| 141 | |||
| 142 | /** |
||
| 143 | * @var float microseconds time of the start of the scan function |
||
| 144 | */ |
||
| 145 | protected $startLoop; |
||
| 146 | |||
| 147 | /** |
||
| 148 | * @var bool Is this retrieved header a multigroup one? |
||
| 149 | */ |
||
| 150 | protected $multiGroup; |
||
| 151 | |||
| 152 | /** |
||
| 153 | * @var bool |
||
| 154 | */ |
||
| 155 | protected $allAsMgr; |
||
| 156 | |||
| 157 | /** |
||
| 158 | * @var string How long it took in seconds to download headers |
||
| 159 | */ |
||
| 160 | protected $timeHeaders; |
||
| 161 | |||
| 162 | /** |
||
| 163 | * @var string How long it took in seconds to clean/parse headers |
||
| 164 | */ |
||
| 165 | protected $timeCleaning; |
||
| 166 | |||
| 167 | /** |
||
| 168 | * @var float microseconds time part repair was started |
||
| 169 | */ |
||
| 170 | protected $startPR; |
||
| 171 | |||
| 172 | /** |
||
| 173 | * @var array The CBP/MGR tables names |
||
| 174 | */ |
||
| 175 | protected $tableNames; |
||
| 176 | |||
| 177 | /** |
||
| 178 | * @var float microseconds time header update was started |
||
| 179 | */ |
||
| 180 | protected $startUpdate; |
||
| 181 | |||
| 182 | /** |
||
| 183 | * @var string The time it took to insert the headers |
||
| 184 | */ |
||
| 185 | protected $timeInsert; |
||
| 186 | |||
| 187 | /** |
||
| 188 | * @var array the header currently being scanned |
||
| 189 | */ |
||
| 190 | protected $header; |
||
| 191 | |||
| 192 | /** |
||
| 193 | * @var bool Should we add parts to part repair queue? |
||
| 194 | */ |
||
| 195 | protected $addToPartRepair; |
||
| 196 | |||
| 197 | /** |
||
| 198 | * @var array Numbers of Headers received from the USP |
||
| 199 | */ |
||
| 200 | protected $headersReceived; |
||
| 201 | |||
| 202 | /** |
||
| 203 | * @var array The current newsgroup information being updated |
||
| 204 | */ |
||
| 205 | protected $groupMySQL; |
||
| 206 | |||
| 207 | /** |
||
| 208 | * @var int the last article number in the range |
||
| 209 | */ |
||
| 210 | protected $last; |
||
| 211 | |||
| 212 | /** |
||
| 213 | * @var int the first article number in the range |
||
| 214 | */ |
||
| 215 | protected $first; |
||
| 216 | |||
| 217 | /** |
||
| 218 | * @var int How many received headers were not yEnc encoded |
||
| 219 | */ |
||
| 220 | protected $notYEnc; |
||
| 221 | |||
| 222 | /** |
||
| 223 | * @var int How many received headers were blacklist matched |
||
| 224 | */ |
||
| 225 | protected $headersBlackListed; |
||
| 226 | |||
| 227 | /** |
||
| 228 | * @var array Header numbers that were not inserted |
||
| 229 | */ |
||
| 230 | protected $headersNotInserted; |
||
| 231 | |||
| 232 | /** |
||
| 233 | * Constructor. |
||
| 234 | * |
||
| 235 | * @param array $options Class instances / echo to CLI? |
||
| 236 | * |
||
| 237 | * @throws \Exception |
||
| 238 | */ |
||
| 239 | public function __construct(array $options = []) |
||
| 240 | { |
||
| 241 | $defaults = [ |
||
| 242 | 'Echo' => true, |
||
| 243 | 'CollectionsCleaning' => null, |
||
| 244 | 'ColorCLI' => null, |
||
| 245 | 'Logger' => null, |
||
| 246 | 'Groups' => null, |
||
| 247 | 'NNTP' => null, |
||
| 248 | 'Settings' => null, |
||
| 249 | ]; |
||
| 250 | $options += $defaults; |
||
| 251 | |||
| 252 | $this->_echoCLI = ($options['Echo'] && config('nntmux.echocli')); |
||
| 253 | |||
| 254 | $this->_pdo = ($options['Settings'] instanceof DB ? $options['Settings'] : new DB()); |
||
| 255 | $this->_colorCLI = ($options['ColorCLI'] instanceof ColorCLI ? $options['ColorCLI'] : new ColorCLI()); |
||
| 256 | $this->_nntp = ($options['NNTP'] instanceof NNTP ? $options['NNTP'] : new NNTP(['Echo' => $this->_colorCLI, 'Settings' => $this->_pdo, 'ColorCLI' => $this->_colorCLI])); |
||
| 257 | $this->_collectionsCleaning = ($options['CollectionsCleaning'] instanceof CollectionsCleaning ? $options['CollectionsCleaning'] : new CollectionsCleaning(['Settings' => $this->_pdo])); |
||
| 258 | |||
| 259 | $this->messageBuffer = Settings::settingValue('..maxmssgs') !== '' ? |
||
|
|
|||
| 260 | (int) Settings::settingValue('..maxmssgs') : 20000; |
||
| 261 | $this->_compressedHeaders = (int) Settings::settingValue('..compressedheaders') === 1; |
||
| 262 | $this->_partRepair = (int) Settings::settingValue('..partrepair') === 1; |
||
| 263 | $this->_newGroupScanByDays = (int) Settings::settingValue('..newgroupscanmethod') === 1; |
||
| 264 | $this->_newGroupMessagesToScan = Settings::settingValue('..newgroupmsgstoscan') !== '' ? (int) Settings::settingValue('..newgroupmsgstoscan') : 50000; |
||
| 265 | $this->_newGroupDaysToScan = Settings::settingValue('..newgroupdaystoscan') !== '' ? (int) Settings::settingValue('..newgroupdaystoscan') : 3; |
||
| 266 | $this->_partRepairLimit = Settings::settingValue('..maxpartrepair') !== '' ? (int) Settings::settingValue('..maxpartrepair') : 15000; |
||
| 267 | $this->_partRepairMaxTries = (Settings::settingValue('..partrepairmaxtries') !== '' ? (int) Settings::settingValue('..partrepairmaxtries') : 3); |
||
| 268 | $this->_showDroppedYEncParts = (int) Settings::settingValue('..showdroppedyencparts') === 1; |
||
| 269 | $this->allAsMgr = (int) Settings::settingValue('..allasmgr') === 1; |
||
| 270 | |||
| 271 | $this->blackList = $this->whiteList = []; |
||
| 272 | } |
||
| 273 | |||
| 274 | /** |
||
| 275 | * Download new headers for all active groups. |
||
| 276 | * |
||
| 277 | * @param int $maxHeaders (Optional) How many headers to download max. |
||
| 278 | * |
||
| 279 | * @return void |
||
| 280 | * @throws \Exception |
||
| 281 | */ |
||
| 282 | public function updateAllGroups($maxHeaders = 100000): void |
||
| 283 | { |
||
| 284 | $groups = Group::getActive(); |
||
| 285 | |||
| 286 | $groupCount = \count($groups); |
||
| 287 | if ($groupCount > 0) { |
||
| 288 | $counter = 1; |
||
| 289 | $allTime = microtime(true); |
||
| 290 | |||
| 291 | $this->log( |
||
| 292 | 'Updating: '.$groupCount.' group(s) - Using compression? '.($this->_compressedHeaders ? 'Yes' : 'No'), |
||
| 293 | __FUNCTION__, |
||
| 294 | 'header' |
||
| 295 | ); |
||
| 296 | |||
| 297 | // Loop through groups. |
||
| 298 | foreach ($groups as $group) { |
||
| 299 | $this->log( |
||
| 300 | 'Starting group '.$counter.' of '.$groupCount, |
||
| 301 | __FUNCTION__, |
||
| 302 | 'header' |
||
| 303 | ); |
||
| 304 | $this->updateGroup($group, $maxHeaders); |
||
| 305 | $counter++; |
||
| 306 | } |
||
| 307 | |||
| 308 | $this->log( |
||
| 309 | 'Updating completed in '.number_format(microtime(true) - $allTime, 2).' seconds.', |
||
| 310 | __FUNCTION__, |
||
| 311 | 'primary' |
||
| 312 | ); |
||
| 313 | } else { |
||
| 314 | $this->log( |
||
| 315 | 'No groups specified. Ensure groups are added to NNTmux\'s database for updating.', |
||
| 316 | __FUNCTION__, |
||
| 317 | 'warning' |
||
| 318 | ); |
||
| 319 | } |
||
| 320 | } |
||
| 321 | |||
| 322 | /** |
||
| 323 | * When the indexer is started, log the date/time. |
||
| 324 | */ |
||
| 325 | public function logIndexerStart(): void |
||
| 326 | { |
||
| 327 | Settings::query()->where('setting', '=', 'last_run_time')->update(['value' => Carbon::now()]); |
||
| 328 | } |
||
| 329 | |||
| 330 | /** |
||
| 331 | * Download new headers for a single group. |
||
| 332 | * |
||
| 333 | * @param array $groupMySQL Array of MySQL results for a single group. |
||
| 334 | * @param int $maxHeaders (Optional) How many headers to download max. |
||
| 335 | * |
||
| 336 | * @return void |
||
| 337 | * @throws \Exception |
||
| 338 | */ |
||
| 339 | public function updateGroup($groupMySQL, $maxHeaders = 0): void |
||
| 340 | { |
||
| 341 | $startGroup = microtime(true); |
||
| 342 | |||
| 343 | $this->logIndexerStart(); |
||
| 344 | |||
| 345 | // Select the group on the NNTP server, gets the latest info on it. |
||
| 346 | $groupNNTP = $this->_nntp->selectGroup($groupMySQL['name']); |
||
| 347 | if ($this->_nntp->isError($groupNNTP)) { |
||
| 348 | $groupNNTP = $this->_nntp->dataError($this->_nntp, $groupMySQL['name']); |
||
| 349 | |||
| 350 | if (isset($groupNNTP['code']) && (int) $groupNNTP['code'] === 411) { |
||
| 351 | Group::disableIfNotExist($groupMySQL['id']); |
||
| 352 | } |
||
| 353 | if ($this->_nntp->isError($groupNNTP)) { |
||
| 354 | return; |
||
| 355 | } |
||
| 356 | } |
||
| 357 | |||
| 358 | if ($this->_echoCLI) { |
||
| 359 | ColorCLI::doEcho(ColorCLI::primary('Processing '.$groupMySQL['name']), true); |
||
| 360 | } |
||
| 361 | |||
| 362 | // Attempt to repair any missing parts before grabbing new ones. |
||
| 363 | if ((int) $groupMySQL['last_record'] !== 0) { |
||
| 364 | if ($this->_partRepair) { |
||
| 365 | if ($this->_echoCLI) { |
||
| 366 | ColorCLI::doEcho(ColorCLI::primary('Part repair enabled. Checking for missing parts.'), true); |
||
| 367 | } |
||
| 368 | $this->partRepair($groupMySQL); |
||
| 369 | |||
| 370 | $mgrPosters = $this->getMultiGroupPosters(); |
||
| 371 | if ($this->allAsMgr === true || ! empty($mgrPosters)) { |
||
| 372 | $tableNames = ProcessReleasesMultiGroup::tableNames(); |
||
| 373 | $this->partRepair($groupMySQL, $tableNames); |
||
| 374 | } |
||
| 375 | } elseif ($this->_echoCLI) { |
||
| 376 | ColorCLI::doEcho(ColorCLI::primary('Part repair disabled by user.'), true); |
||
| 377 | } |
||
| 378 | } |
||
| 379 | |||
| 380 | // Generate postdate for first record, for those that upgraded. |
||
| 381 | if ($groupMySQL['first_record_postdate'] === null && (int) $groupMySQL['first_record'] !== 0) { |
||
| 382 | $groupMySQL['first_record_postdate'] = $this->postdate($groupMySQL['first_record'], $groupNNTP); |
||
| 383 | Group::query()->where('id', $groupMySQL['id'])->update(['first_record_postdate' => Carbon::createFromTimestamp($groupMySQL['first_record_postdate'])]); |
||
| 384 | } |
||
| 385 | |||
| 386 | // Get first article we want aka the oldest. |
||
| 387 | if ((int) $groupMySQL['last_record'] === 0) { |
||
| 388 | if ($this->_newGroupScanByDays) { |
||
| 389 | // For new newsgroups - determine here how far we want to go back using date. |
||
| 390 | $first = $this->daytopost($this->_newGroupDaysToScan, $groupNNTP); |
||
| 391 | } elseif ($groupNNTP['first'] >= ($groupNNTP['last'] - ($this->_newGroupMessagesToScan + $this->messageBuffer))) { |
||
| 392 | // If what we want is lower than the groups first article, set the wanted first to the first. |
||
| 393 | $first = $groupNNTP['first']; |
||
| 394 | } else { |
||
| 395 | // Or else, use the newest article minus how much we should get for new groups. |
||
| 396 | $first = (string) ($groupNNTP['last'] - ($this->_newGroupMessagesToScan + $this->messageBuffer)); |
||
| 397 | } |
||
| 398 | |||
| 399 | // We will use this to subtract so we leave articles for the next time (in case the server doesn't have them yet) |
||
| 400 | $leaveOver = $this->messageBuffer; |
||
| 401 | |||
| 402 | // If this is not a new group, go from our newest to the servers newest. |
||
| 403 | } else { |
||
| 404 | // Set our oldest wanted to our newest local article. |
||
| 405 | $first = $groupMySQL['last_record']; |
||
| 406 | |||
| 407 | // This is how many articles we will grab. (the servers newest minus our newest). |
||
| 408 | $totalCount = (string) ($groupNNTP['last'] - $first); |
||
| 409 | |||
| 410 | // Check if the server has more articles than our loop limit x 2. |
||
| 411 | if ($totalCount > ($this->messageBuffer * 2)) { |
||
| 412 | // Get the remainder of $totalCount / $this->message buffer |
||
| 413 | $leaveOver = round($totalCount % $this->messageBuffer, 0, PHP_ROUND_HALF_DOWN) + $this->messageBuffer; |
||
| 414 | } else { |
||
| 415 | // Else get half of the available. |
||
| 416 | $leaveOver = round($totalCount / 2, 0, PHP_ROUND_HALF_DOWN); |
||
| 417 | } |
||
| 418 | } |
||
| 419 | |||
| 420 | // The last article we want, aka the newest. |
||
| 421 | $last = $groupLast = (string) ($groupNNTP['last'] - $leaveOver); |
||
| 422 | |||
| 423 | // If the newest we want is older than the oldest we want somehow.. set them equal. |
||
| 424 | if ($last < $first) { |
||
| 425 | $last = $groupLast = $first; |
||
| 426 | } |
||
| 427 | |||
| 428 | // This is how many articles we are going to get. |
||
| 429 | $total = (string) ($groupLast - $first); |
||
| 430 | // This is how many articles are available (without $leaveOver). |
||
| 431 | $realTotal = (string) ($groupNNTP['last'] - $first); |
||
| 432 | |||
| 433 | // Check if we should limit the amount of fetched new headers. |
||
| 434 | if ($maxHeaders > 0) { |
||
| 435 | if ($maxHeaders < ($groupLast - $first)) { |
||
| 436 | $groupLast = $last = (string) ($first + $maxHeaders); |
||
| 437 | } |
||
| 438 | $total = (string) ($groupLast - $first); |
||
| 439 | } |
||
| 440 | |||
| 441 | // If total is bigger than 0 it means we have new parts in the newsgroup. |
||
| 442 | if ($total > 0) { |
||
| 443 | if ($this->_echoCLI) { |
||
| 444 | ColorCLI::doEcho( |
||
| 445 | ColorCLI::primary( |
||
| 446 | ( |
||
| 447 | (int) $groupMySQL['last_record'] === 0 |
||
| 448 | ? 'New group '.$groupNNTP['group'].' starting with '. |
||
| 449 | ( |
||
| 450 | $this->_newGroupScanByDays |
||
| 451 | ? $this->_newGroupDaysToScan.' days' |
||
| 452 | : number_format($this->_newGroupMessagesToScan).' messages' |
||
| 453 | ).' worth.' |
||
| 454 | : 'Group '.$groupNNTP['group'].' has '.number_format($realTotal).' new articles.' |
||
| 455 | ). |
||
| 456 | ' Leaving '.number_format($leaveOver). |
||
| 457 | " for next pass.\nServer oldest: ".number_format($groupNNTP['first']). |
||
| 458 | ' Server newest: '.number_format($groupNNTP['last']). |
||
| 459 | ' Local newest: '.number_format($groupMySQL['last_record']) |
||
| 460 | ), |
||
| 461 | true |
||
| 462 | ); |
||
| 463 | } |
||
| 464 | |||
| 465 | $done = false; |
||
| 466 | // Get all the parts (in portions of $this->messageBuffer to not use too much memory). |
||
| 467 | while ($done === false) { |
||
| 468 | |||
| 469 | // Increment last until we reach $groupLast (group newest article). |
||
| 470 | if ($total > $this->messageBuffer) { |
||
| 471 | if ((string) ($first + $this->messageBuffer) > $groupLast) { |
||
| 472 | $last = $groupLast; |
||
| 473 | } else { |
||
| 474 | $last = (string) ($first + $this->messageBuffer); |
||
| 475 | } |
||
| 476 | } |
||
| 477 | // Increment first so we don't get an article we already had. |
||
| 478 | $first++; |
||
| 479 | |||
| 480 | if ($this->_echoCLI) { |
||
| 481 | ColorCLI::doEcho( |
||
| 482 | ColorCLI::header( |
||
| 483 | PHP_EOL.'Getting '.number_format($last - $first + 1).' articles ('.number_format($first). |
||
| 484 | ' to '.number_format($last).') from '.$groupMySQL['name'].' - ('. |
||
| 485 | number_format($groupLast - $last).' articles in queue).' |
||
| 486 | ) |
||
| 487 | ); |
||
| 488 | } |
||
| 489 | |||
| 490 | // Get article headers from newsgroup. |
||
| 491 | $scanSummary = $this->scan($groupMySQL, $first, $last); |
||
| 492 | |||
| 493 | // Check if we fetched headers. |
||
| 494 | if (! empty($scanSummary)) { |
||
| 495 | |||
| 496 | // If new group, update first record & postdate |
||
| 497 | if ($groupMySQL['first_record_postdate'] === null && (int) $groupMySQL['first_record'] === 0) { |
||
| 498 | $groupMySQL['first_record'] = $scanSummary['firstArticleNumber']; |
||
| 499 | |||
| 500 | if (isset($scanSummary['firstArticleDate'])) { |
||
| 501 | $groupMySQL['first_record_postdate'] = strtotime($scanSummary['firstArticleDate']); |
||
| 502 | } else { |
||
| 503 | $groupMySQL['first_record_postdate'] = $this->postdate($groupMySQL['first_record'], $groupNNTP); |
||
| 504 | } |
||
| 505 | |||
| 506 | Group::query() |
||
| 507 | ->where('id', $groupMySQL['id']) |
||
| 508 | ->update( |
||
| 509 | [ |
||
| 510 | 'first_record' => $scanSummary['firstArticleNumber'], |
||
| 511 | 'first_record_postdate' => Carbon::createFromTimestamp( |
||
| 512 | $groupMySQL['first_record_postdate'] |
||
| 513 | ), |
||
| 514 | ] |
||
| 515 | ); |
||
| 516 | } |
||
| 517 | |||
| 518 | $scanSummary['lastArticleDate'] = (isset($scanSummary['lastArticleDate']) ? strtotime($scanSummary['lastArticleDate']) : false); |
||
| 519 | if (! is_numeric($scanSummary['lastArticleDate'])) { |
||
| 520 | $scanSummary['lastArticleDate'] = $this->postdate($scanSummary['lastArticleNumber'], $groupNNTP); |
||
| 521 | } |
||
| 522 | |||
| 523 | Group::query() |
||
| 524 | ->where('id', $groupMySQL['id']) |
||
| 525 | ->update( |
||
| 526 | [ |
||
| 527 | 'last_record' => $scanSummary['lastArticleNumber'], |
||
| 528 | 'last_record_postdate' => Carbon::createFromTimestamp($scanSummary['lastArticleDate']), |
||
| 529 | 'last_updated' => Carbon::now(), |
||
| 530 | ] |
||
| 531 | ); |
||
| 532 | } else { |
||
| 533 | // If we didn't fetch headers, update the record still. |
||
| 534 | Group::query() |
||
| 535 | ->where('id', $groupMySQL['id']) |
||
| 536 | ->update( |
||
| 537 | [ |
||
| 538 | 'last_record' => $last, |
||
| 539 | 'last_updated' => Carbon::now(), |
||
| 540 | ] |
||
| 541 | ); |
||
| 542 | } |
||
| 543 | |||
| 544 | if ((int) $last === (int) $groupLast) { |
||
| 545 | $done = true; |
||
| 546 | } else { |
||
| 547 | $first = $last; |
||
| 548 | } |
||
| 549 | } |
||
| 550 | |||
| 551 | if ($this->_echoCLI) { |
||
| 552 | ColorCLI::doEcho( |
||
| 553 | ColorCLI::primary( |
||
| 554 | PHP_EOL.'Group '.$groupMySQL['name'].' processed in '. |
||
| 555 | number_format(microtime(true) - $startGroup, 2).' seconds.' |
||
| 556 | ), |
||
| 557 | true |
||
| 558 | ); |
||
| 559 | } |
||
| 560 | } elseif ($this->_echoCLI) { |
||
| 561 | ColorCLI::doEcho( |
||
| 562 | ColorCLI::primary( |
||
| 563 | 'No new articles for '.$groupMySQL['name'].' (first '.number_format($first). |
||
| 564 | ', last '.number_format($last).', grouplast '.number_format($groupMySQL['last_record']). |
||
| 565 | ', total '.number_format($total).")\n".'Server oldest: '.number_format($groupNNTP['first']). |
||
| 566 | ' Server newest: '.number_format($groupNNTP['last']).' Local newest: '.number_format($groupMySQL['last_record']) |
||
| 567 | ), |
||
| 568 | true |
||
| 569 | ); |
||
| 570 | } |
||
| 571 | } |
||
| 572 | |||
| 573 | /** |
||
| 574 | * Loop over range of wanted headers, insert headers into DB. |
||
| 575 | * |
||
| 576 | * @param array $groupMySQL The group info from mysql. |
||
| 577 | * @param int $first The oldest wanted header. |
||
| 578 | * @param int $last The newest wanted header. |
||
| 579 | * @param string $type Is this partrepair or update or backfill? |
||
| 580 | * @param null|array $missingParts If we are running in partrepair, the list of missing article numbers. |
||
| 581 | * |
||
| 582 | * @return array Empty on failure. |
||
| 583 | * @throws \Exception |
||
| 584 | */ |
||
| 585 | public function scan($groupMySQL, $first, $last, $type = 'update', $missingParts = null): array |
||
| 586 | { |
||
| 587 | // Start time of scan method and of fetching headers. |
||
| 588 | $this->startLoop = microtime(true); |
||
| 589 | $this->groupMySQL = $groupMySQL; |
||
| 590 | $this->last = $last; |
||
| 591 | $this->first = $first; |
||
| 592 | |||
| 593 | $this->notYEnc = $this->headersBlackListed = 0; |
||
| 594 | |||
| 595 | // Check if MySQL tables exist, create if they do not, get their names at the same time. |
||
| 596 | $this->tableNames = (new Group())->getCBPTableNames($this->groupMySQL['id']); |
||
| 597 | |||
| 598 | $mgrPosters = $this->getMultiGroupPosters(); |
||
| 599 | |||
| 600 | if ($this->allAsMgr === true || ! empty($mgrPosters)) { |
||
| 601 | $mgrActive = true; |
||
| 602 | $mgrPosters = ! empty($mgrPosters) ? array_flip(array_column($mgrPosters, 'poster')) : ''; |
||
| 603 | } else { |
||
| 604 | $mgrActive = false; |
||
| 605 | } |
||
| 606 | |||
| 607 | $returnArray = $stdHeaders = $mgrHeaders = []; |
||
| 608 | |||
| 609 | $partRepair = ($type === 'partrepair'); |
||
| 610 | $this->addToPartRepair = ($type === 'update' && $this->_partRepair); |
||
| 611 | |||
| 612 | // Download the headers. |
||
| 613 | if ($partRepair === true) { |
||
| 614 | // This is slower but possibly is better with missing headers. |
||
| 615 | $headers = $this->_nntp->getOverview($this->first.'-'.$this->last, true, false); |
||
| 616 | } else { |
||
| 617 | $headers = $this->_nntp->getXOVER($this->first.'-'.$this->last); |
||
| 618 | } |
||
| 619 | |||
| 620 | // If there was an error, try to reconnect. |
||
| 621 | if ($this->_nntp->isError($headers)) { |
||
| 622 | |||
| 623 | // Increment if part repair and return false. |
||
| 624 | if ($partRepair === true) { |
||
| 625 | $this->_pdo->queryExec( |
||
| 626 | sprintf( |
||
| 627 | 'UPDATE %s SET attempts = attempts + 1 WHERE groups_id = %d AND numberid %s', |
||
| 628 | $this->tableNames['prname'], |
||
| 629 | $this->groupMySQL['id'], |
||
| 630 | ((int) $this->first === (int) $this->last ? '= '.$this->first : 'IN ('.implode(',', range($this->first, $this->last)).')') |
||
| 631 | ) |
||
| 632 | ); |
||
| 633 | |||
| 634 | return $returnArray; |
||
| 635 | } |
||
| 636 | |||
| 637 | // This is usually a compression error, so try disabling compression. |
||
| 638 | $this->_nntp->doQuit(); |
||
| 639 | if ($this->_nntp->doConnect(false) !== true) { |
||
| 640 | return $returnArray; |
||
| 641 | } |
||
| 642 | |||
| 643 | // Re-select group, download headers again without compression and re-enable compression. |
||
| 644 | $this->_nntp->selectGroup($this->groupMySQL['name']); |
||
| 645 | $headers = $this->_nntp->getXOVER($this->first.'-'.$this->last); |
||
| 646 | $this->_nntp->enableCompression(); |
||
| 647 | |||
| 648 | // Check if the non-compression headers have an error. |
||
| 649 | if ($this->_nntp->isError($headers)) { |
||
| 650 | $message = ((int) $headers->code === 0 ? 'Unknown error' : $headers->message); |
||
| 651 | $this->log( |
||
| 652 | "Code {$headers->code}: $message\nSkipping group: {$this->groupMySQL['name']}", |
||
| 653 | __FUNCTION__, |
||
| 654 | 'error' |
||
| 655 | ); |
||
| 656 | |||
| 657 | return $returnArray; |
||
| 658 | } |
||
| 659 | } |
||
| 660 | |||
| 661 | // Start of processing headers. |
||
| 662 | $this->startCleaning = microtime(true); |
||
| 663 | |||
| 664 | // End of the getting data from usenet. |
||
| 665 | $this->timeHeaders = number_format($this->startCleaning - $this->startLoop, 2); |
||
| 666 | |||
| 667 | // Check if we got headers. |
||
| 668 | $msgCount = \count($headers); |
||
| 669 | |||
| 670 | if ($msgCount < 1) { |
||
| 671 | return $returnArray; |
||
| 672 | } |
||
| 673 | |||
| 674 | $this->getHighLowArticleInfo($returnArray, $headers, $msgCount); |
||
| 675 | |||
| 676 | $headersRepaired = $rangeNotReceived = $this->headersReceived = $this->headersNotInserted = []; |
||
| 677 | |||
| 678 | foreach ($headers as $header) { |
||
| 679 | |||
| 680 | // Check if we got the article or not. |
||
| 681 | if (isset($header['Number'])) { |
||
| 682 | $this->headersReceived[] = $header['Number']; |
||
| 683 | } else { |
||
| 684 | if ($this->addToPartRepair) { |
||
| 685 | $rangeNotReceived[] = $header['Number']; |
||
| 686 | } |
||
| 687 | continue; |
||
| 688 | } |
||
| 689 | |||
| 690 | // If set we are running in partRepair mode. |
||
| 691 | if ($partRepair === true && $missingParts !== null) { |
||
| 692 | if (! \in_array($header['Number'], $missingParts, false)) { |
||
| 693 | // If article isn't one that is missing skip it. |
||
| 694 | continue; |
||
| 695 | } |
||
| 696 | // We got the part this time. Remove article from part repair. |
||
| 697 | $headersRepaired[] = $header['Number']; |
||
| 698 | } |
||
| 699 | |||
| 700 | /* |
||
| 701 | * Find part / total parts. Ignore if no part count found. |
||
| 702 | * |
||
| 703 | * \s* Trims the leading space. |
||
| 704 | * (?!"Usenet Index Post) ignores these types of articles, they are useless. |
||
| 705 | * (.+) Fetches the subject. |
||
| 706 | * \s+ Trims trailing space after the subject. |
||
| 707 | * \((\d+)\/(\d+)\) Gets the part count. |
||
| 708 | * No ending ($) as there are cases of subjects with extra data after the part count. |
||
| 709 | */ |
||
| 710 | if (preg_match('/^\s*(?!"Usenet Index Post)(.+)\s+\((\d+)\/(\d+)\)/', $header['Subject'], $header['matches'])) { |
||
| 711 | // Add yEnc to subjects that do not have them, but have the part number at the end of the header. |
||
| 712 | if (stripos($header['Subject'], 'yEnc') === false) { |
||
| 713 | $header['matches'][1] .= ' yEnc'; |
||
| 714 | } |
||
| 715 | } else { |
||
| 716 | if ($this->_showDroppedYEncParts === true && strpos($header['Subject'], '"Usenet Index Post') !== 0) { |
||
| 717 | file_put_contents( |
||
| 718 | NN_LOGS.'not_yenc'.$this->groupMySQL['name'].'.dropped.log', |
||
| 719 | $header['Subject'].PHP_EOL, |
||
| 720 | FILE_APPEND |
||
| 721 | ); |
||
| 722 | } |
||
| 723 | $this->notYEnc++; |
||
| 724 | continue; |
||
| 725 | } |
||
| 726 | |||
| 727 | // Filter subject based on black/white list. |
||
| 728 | if ($this->isBlackListed($header, $this->groupMySQL['name'])) { |
||
| 729 | $this->headersBlackListed++; |
||
| 730 | continue; |
||
| 731 | } |
||
| 732 | |||
| 733 | if (! isset($header['Bytes'])) { |
||
| 734 | $header['Bytes'] = (isset($this->header[':bytes']) ? $header[':bytes'] : 0); |
||
| 735 | } |
||
| 736 | $header['Bytes'] = (int) $header['Bytes']; |
||
| 737 | |||
| 738 | if ($this->allAsMgr === true || ($mgrActive === true && array_key_exists($header['From'], $mgrPosters))) { |
||
| 739 | $mgrHeaders[] = $header; |
||
| 740 | } else { |
||
| 741 | $stdHeaders[] = $header; |
||
| 742 | } |
||
| 743 | } |
||
| 744 | |||
| 745 | unset($headers); // Reclaim memory now that headers are split. |
||
| 746 | |||
| 747 | if (! empty($this->_binaryBlacklistIdsToUpdate)) { |
||
| 748 | $this->updateBlacklistUsage(); |
||
| 749 | } |
||
| 750 | |||
| 751 | if ($this->_echoCLI && $partRepair === false) { |
||
| 752 | $this->outputHeaderInitial(); |
||
| 753 | } |
||
| 754 | |||
| 755 | // MGR headers goes first |
||
| 756 | if (! empty($mgrHeaders)) { |
||
| 757 | $this->tableNames = ProcessReleasesMultiGroup::tableNames(); |
||
| 758 | $this->storeHeaders($mgrHeaders, true); |
||
| 759 | } |
||
| 760 | unset($mgrHeaders); |
||
| 761 | |||
| 762 | // Standard headers go second so we can switch tableNames back and do part repair to standard group tables |
||
| 763 | if (! empty($stdHeaders)) { |
||
| 764 | $this->tableNames = (new Group())->getCBPTableNames($this->groupMySQL['id']); |
||
| 765 | $this->storeHeaders($stdHeaders, false); |
||
| 766 | } |
||
| 767 | unset($stdHeaders); |
||
| 768 | |||
| 769 | // Start of part repair. |
||
| 770 | $this->startPR = microtime(true); |
||
| 771 | |||
| 772 | // End of inserting. |
||
| 773 | $this->timeInsert = number_format($this->startPR - $this->startUpdate, 2); |
||
| 774 | |||
| 775 | if ($partRepair && \count($headersRepaired) > 0) { |
||
| 776 | $this->removeRepairedParts($headersRepaired, $this->tableNames['prname'], $this->groupMySQL['id']); |
||
| 777 | } |
||
| 778 | unset($headersRepaired); |
||
| 779 | |||
| 780 | if ($this->addToPartRepair) { |
||
| 781 | $notInsertedCount = \count($this->headersNotInserted); |
||
| 782 | if ($notInsertedCount > 0) { |
||
| 783 | $this->addMissingParts($this->headersNotInserted, $this->tableNames['prname'], $this->groupMySQL['id']); |
||
| 784 | |||
| 785 | $this->log( |
||
| 786 | $notInsertedCount.' articles failed to insert!', |
||
| 787 | __FUNCTION__, |
||
| 788 | 'warning' |
||
| 789 | ); |
||
| 790 | } |
||
| 791 | unset($this->headersNotInserted); |
||
| 792 | |||
| 793 | // Check if we have any missing headers. |
||
| 794 | if (($this->last - $this->first - $this->notYEnc - $this->headersBlackListed + 1) > \count($this->headersReceived)) { |
||
| 795 | $rangeNotReceived = array_merge($rangeNotReceived, array_diff(range($this->first, $this->last), $this->headersReceived)); |
||
| 796 | } |
||
| 797 | $notReceivedCount = \count($rangeNotReceived); |
||
| 798 | if ($notReceivedCount > 0) { |
||
| 799 | $this->addMissingParts($rangeNotReceived, $this->tableNames['prname'], $this->groupMySQL['id']); |
||
| 800 | |||
| 801 | if ($this->_echoCLI) { |
||
| 802 | ColorCLI::doEcho( |
||
| 803 | ColorCLI::alternate( |
||
| 804 | 'Server did not return '.$notReceivedCount. |
||
| 805 | ' articles from '.$this->groupMySQL['name'].'.' |
||
| 806 | ), |
||
| 807 | true |
||
| 808 | ); |
||
| 809 | } |
||
| 810 | } |
||
| 811 | unset($rangeNotReceived); |
||
| 812 | } |
||
| 813 | |||
| 814 | $this->outputHeaderDuration(); |
||
| 815 | |||
| 816 | return $returnArray; |
||
| 817 | } |
||
| 818 | |||
| 819 | /** |
||
| 820 | * Parse headers into collections/binaries and store header data as parts. |
||
| 821 | * |
||
| 822 | * @param array $headers The retrieved headers |
||
| 823 | * @param bool $multiGroup Is this task being run in MGR mode? |
||
| 824 | * |
||
| 825 | * @throws \Exception |
||
| 826 | */ |
||
| 827 | protected function storeHeaders(array $headers, $multiGroup): void |
||
| 828 | { |
||
| 829 | $this->multiGroup = $multiGroup; |
||
| 830 | $binariesUpdate = $collectionIDs = $articles = []; |
||
| 831 | |||
| 832 | $this->_pdo->beginTransaction(); |
||
| 833 | |||
| 834 | $partsQuery = $partsCheck = |
||
| 835 | "INSERT IGNORE INTO {$this->tableNames['pname']} (binaries_id, number, messageid, partnumber, size) VALUES "; |
||
| 836 | |||
| 837 | // Loop articles, figure out files/parts. |
||
| 838 | foreach ($headers as $this->header) { |
||
| 839 | // Set up the info for inserting into parts/binaries/collections tables. |
||
| 840 | if (! isset($articles[$this->header['matches'][1]])) { |
||
| 841 | |||
| 842 | // check whether file count should be ignored (XXX packs for now only). |
||
| 843 | $whitelistMatch = false; |
||
| 844 | if ($this->_ignoreFileCount($this->groupMySQL['name'], $this->header['matches'][1])) { |
||
| 845 | $whitelistMatch = true; |
||
| 846 | $fileCount[1] = $fileCount[3] = 0; |
||
| 847 | } |
||
| 848 | |||
| 849 | // Attempt to find the file count. If it is not found, set it to 0. |
||
| 850 | if (! $whitelistMatch && ! preg_match('/[[(\s](\d{1,5})(\/|[\s_]of[\s_]|-)(\d{1,5})[])\s$:]/i', $this->header['matches'][1], $fileCount)) { |
||
| 851 | $fileCount[1] = $fileCount[3] = 0; |
||
| 852 | if ($this->_showDroppedYEncParts === true) { |
||
| 853 | file_put_contents( |
||
| 854 | NN_LOGS.'no_files'.$this->groupMySQL['name'].'.log', |
||
| 855 | $this->header['Subject'].PHP_EOL, |
||
| 856 | FILE_APPEND |
||
| 857 | ); |
||
| 858 | } |
||
| 859 | } |
||
| 860 | |||
| 861 | if ($this->multiGroup) { |
||
| 862 | $ckName = ''; |
||
| 863 | $ckId = ''; |
||
| 864 | } else { |
||
| 865 | $ckName = $this->groupMySQL['name']; |
||
| 866 | $ckId = $this->groupMySQL['id']; |
||
| 867 | } |
||
| 868 | |||
| 869 | $collMatch = $this->_collectionsCleaning->collectionsCleaner( |
||
| 870 | $this->header['matches'][1], |
||
| 871 | $ckName |
||
| 872 | ); |
||
| 873 | |||
| 874 | // Used to group articles together when forming the release. MGR requires this to be group irrespective |
||
| 875 | $this->header['CollectionKey'] = $collMatch['name'].$ckId.$fileCount[3]; |
||
| 876 | |||
| 877 | // If this header's collection key isn't in memory, attempt to insert the collection |
||
| 878 | if (! isset($collectionIDs[$this->header['CollectionKey']])) { |
||
| 879 | |||
| 880 | /* Date from header should be a string this format: |
||
| 881 | * 31 Mar 2014 15:36:04 GMT or 6 Oct 1998 04:38:40 -0500 |
||
| 882 | * Still make sure it's not unix time, convert it to unix time if it is. |
||
| 883 | */ |
||
| 884 | $this->header['Date'] = (is_numeric($this->header['Date']) ? $this->header['Date'] : strtotime($this->header['Date'])); |
||
| 885 | |||
| 886 | // Get the current unixtime from PHP. |
||
| 887 | $now = Carbon::now()->timestamp; |
||
| 888 | |||
| 889 | $xref = ($this->multiGroup === true ? sprintf('xref = CONCAT(xref, "\\n"%s ),', $this->_pdo->escapeString(substr($this->header['Xref'], 2, 255))) : ''); |
||
| 890 | $date = $this->header['Date'] > $now ? $now : $this->header['Date']; |
||
| 891 | $unixtime = is_numeric($this->header['Date']) ? $date : $now; |
||
| 892 | |||
| 893 | $random = random_bytes(16); |
||
| 894 | |||
| 895 | $collectionID = $this->_pdo->queryInsert( |
||
| 896 | sprintf( |
||
| 897 | " |
||
| 898 | INSERT INTO %s (subject, fromname, date, xref, groups_id, |
||
| 899 | totalfiles, collectionhash, collection_regexes_id, dateadded) |
||
| 900 | VALUES (%s, %s, FROM_UNIXTIME(%s), %s, %d, %d, '%s', %d, NOW()) |
||
| 901 | ON DUPLICATE KEY UPDATE %s dateadded = NOW(), noise = '%s'", |
||
| 902 | $this->tableNames['cname'], |
||
| 903 | $this->_pdo->escapeString(substr(utf8_encode($this->header['matches'][1]), 0, 255)), |
||
| 904 | $this->_pdo->escapeString(utf8_encode($this->header['From'])), |
||
| 905 | $unixtime, |
||
| 906 | $this->_pdo->escapeString(substr($this->header['Xref'], 0, 255)), |
||
| 907 | $this->groupMySQL['id'], |
||
| 908 | $fileCount[3], |
||
| 909 | sha1($this->header['CollectionKey']), |
||
| 910 | $collMatch['id'], |
||
| 911 | $xref, |
||
| 912 | sodium_bin2hex($random) |
||
| 913 | ) |
||
| 914 | ); |
||
| 915 | |||
| 916 | if ($collectionID === false) { |
||
| 917 | if ($this->addToPartRepair) { |
||
| 918 | $this->headersNotInserted[] = $this->header['Number']; |
||
| 919 | } |
||
| 920 | $this->_pdo->Rollback(); |
||
| 921 | $this->_pdo->beginTransaction(); |
||
| 922 | continue; |
||
| 923 | } |
||
| 924 | $collectionIDs[$this->header['CollectionKey']] = $collectionID; |
||
| 925 | } else { |
||
| 926 | $collectionID = $collectionIDs[$this->header['CollectionKey']]; |
||
| 927 | } |
||
| 928 | |||
| 929 | // MGR or Standard, Binary Hash should be unique to the group |
||
| 930 | $hash = md5($this->header['matches'][1].$this->header['From'].$this->groupMySQL['id']); |
||
| 931 | |||
| 932 | $binaryID = $this->_pdo->queryInsert( |
||
| 933 | sprintf( |
||
| 934 | " |
||
| 935 | INSERT INTO %s (binaryhash, name, collections_id, totalparts, currentparts, filenumber, partsize) |
||
| 936 | VALUES (UNHEX('%s'), %s, %d, %d, 1, %d, %d) |
||
| 937 | ON DUPLICATE KEY UPDATE currentparts = currentparts + 1, partsize = partsize + %d", |
||
| 938 | $this->tableNames['bname'], |
||
| 939 | $hash, |
||
| 940 | $this->_pdo->escapeString(utf8_encode($this->header['matches'][1])), |
||
| 941 | $collectionID, |
||
| 942 | $this->header['matches'][3], |
||
| 943 | $fileCount[1], |
||
| 944 | $this->header['Bytes'], |
||
| 945 | $this->header['Bytes'] |
||
| 946 | ) |
||
| 947 | ); |
||
| 948 | |||
| 949 | if ($binaryID === false) { |
||
| 950 | if ($this->addToPartRepair) { |
||
| 951 | $this->headersNotInserted[] = $this->header['Number']; |
||
| 952 | } |
||
| 953 | $this->_pdo->Rollback(); |
||
| 954 | $this->_pdo->beginTransaction(); |
||
| 955 | continue; |
||
| 956 | } |
||
| 957 | |||
| 958 | $binariesUpdate[$binaryID]['Size'] = 0; |
||
| 959 | $binariesUpdate[$binaryID]['Parts'] = 0; |
||
| 960 | |||
| 961 | $articles[$this->header['matches'][1]]['CollectionID'] = $collectionID; |
||
| 962 | $articles[$this->header['matches'][1]]['BinaryID'] = $binaryID; |
||
| 963 | } else { |
||
| 964 | $binaryID = $articles[$this->header['matches'][1]]['BinaryID']; |
||
| 965 | $binariesUpdate[$binaryID]['Size'] += $this->header['Bytes']; |
||
| 966 | $binariesUpdate[$binaryID]['Parts']++; |
||
| 967 | } |
||
| 968 | |||
| 969 | // Strip the < and >, saves space in DB. |
||
| 970 | $this->header['Message-ID'][0] = "'"; |
||
| 971 | |||
| 972 | $partsQuery .= |
||
| 973 | '('.$binaryID.','.$this->header['Number'].','.rtrim($this->header['Message-ID'], '>')."',". |
||
| 974 | $this->header['matches'][2].','.$this->header['Bytes'].'),'; |
||
| 975 | } |
||
| 976 | |||
| 977 | unset($headers); // Reclaim memory. |
||
| 978 | |||
| 979 | // Start of inserting into SQL. |
||
| 980 | $this->startUpdate = microtime(true); |
||
| 981 | |||
| 982 | // End of processing headers. |
||
| 983 | $this->timeCleaning = number_format($this->startUpdate - $this->startCleaning, 2); |
||
| 984 | $binariesQuery = $binariesCheck = sprintf('INSERT INTO %s (id, partsize, currentparts) VALUES ', $this->tableNames['bname']); |
||
| 985 | foreach ($binariesUpdate as $binaryID => $binary) { |
||
| 986 | $binariesQuery .= '('.$binaryID.','.$binary['Size'].','.$binary['Parts'].'),'; |
||
| 987 | } |
||
| 988 | $binariesEnd = ' ON DUPLICATE KEY UPDATE partsize = VALUES(partsize) + partsize, currentparts = VALUES(currentparts) + currentparts'; |
||
| 989 | $binariesQuery = rtrim($binariesQuery, ',').$binariesEnd; |
||
| 990 | |||
| 991 | // Check if we got any binaries. If we did, try to insert them. |
||
| 992 | if (\strlen($binariesCheck.$binariesEnd) === \strlen($binariesQuery) ? true : $this->_pdo->queryExec($binariesQuery)) { |
||
| 993 | if ($this->_debug) { |
||
| 994 | ColorCLI::doEcho( |
||
| 995 | ColorCLI::debug( |
||
| 996 | 'Sending '.round(\strlen($partsQuery) / 1024, 2). |
||
| 997 | ' KB of'.($this->multiGroup ? ' MGR' : '').' parts to MySQL' |
||
| 998 | ), true |
||
| 999 | ); |
||
| 1000 | } |
||
| 1001 | if (\strlen($partsQuery) === \strlen($partsCheck) ? true : $this->_pdo->queryExec(rtrim($partsQuery, ','))) { |
||
| 1002 | $this->_pdo->Commit(); |
||
| 1003 | } else { |
||
| 1004 | if ($this->addToPartRepair) { |
||
| 1005 | $this->headersNotInserted += $this->headersReceived; |
||
| 1006 | } |
||
| 1007 | $this->_pdo->Rollback(); |
||
| 1008 | } |
||
| 1009 | } else { |
||
| 1010 | if ($this->addToPartRepair) { |
||
| 1011 | $this->headersNotInserted += $this->headersReceived; |
||
| 1012 | } |
||
| 1013 | $this->_pdo->Rollback(); |
||
| 1014 | } |
||
| 1015 | } |
||
| 1016 | |||
| 1017 | /** |
||
| 1018 | * Gets the First and Last Article Number and Date for the received headers. |
||
| 1019 | * |
||
| 1020 | * @param array $returnArray |
||
| 1021 | * @param array $headers |
||
| 1022 | * @param int $msgCount |
||
| 1023 | */ |
||
| 1024 | protected function getHighLowArticleInfo(array &$returnArray, array $headers, int $msgCount): void |
||
| 1025 | { |
||
| 1026 | // Get highest and lowest article numbers/dates. |
||
| 1027 | $iterator1 = 0; |
||
| 1028 | $iterator2 = $msgCount - 1; |
||
| 1029 | while (true) { |
||
| 1030 | if (! isset($returnArray['firstArticleNumber']) && isset($headers[$iterator1]['Number'])) { |
||
| 1031 | $returnArray['firstArticleNumber'] = $headers[$iterator1]['Number']; |
||
| 1032 | $returnArray['firstArticleDate'] = $headers[$iterator1]['Date']; |
||
| 1033 | } |
||
| 1034 | |||
| 1035 | if (! isset($returnArray['lastArticleNumber']) && isset($headers[$iterator2]['Number'])) { |
||
| 1036 | $returnArray['lastArticleNumber'] = $headers[$iterator2]['Number']; |
||
| 1037 | $returnArray['lastArticleDate'] = $headers[$iterator2]['Date']; |
||
| 1038 | } |
||
| 1039 | |||
| 1040 | // Break if we found non empty articles. |
||
| 1041 | if (isset($returnArray['firstArticleNumber, lastArticleNumber'])) { |
||
| 1042 | break; |
||
| 1043 | } |
||
| 1044 | |||
| 1045 | // Break out if we couldn't find anything. |
||
| 1046 | if ($iterator1++ >= $msgCount - 1 || $iterator2-- <= 0) { |
||
| 1047 | break; |
||
| 1048 | } |
||
| 1049 | } |
||
| 1050 | } |
||
| 1051 | |||
| 1052 | /** |
||
| 1053 | * Updates Blacklist Regex Timers in DB to reflect last usage. |
||
| 1054 | */ |
||
| 1055 | protected function updateBlacklistUsage(): void |
||
| 1056 | { |
||
| 1057 | BinaryBlacklist::query()->whereIn('id', $this->_binaryBlacklistIdsToUpdate)->update(['last_activity' => Carbon::now()]); |
||
| 1058 | $this->_binaryBlacklistIdsToUpdate = []; |
||
| 1059 | } |
||
| 1060 | |||
| 1061 | /** |
||
| 1062 | * Outputs the initial header scan results after yEnc check and blacklist routines. |
||
| 1063 | */ |
||
| 1064 | protected function outputHeaderInitial(): void |
||
| 1065 | { |
||
| 1066 | ColorCLI::doEcho( |
||
| 1067 | ColorCLI::primary( |
||
| 1068 | 'Received '.\count($this->headersReceived). |
||
| 1069 | ' articles of '.number_format($this->last - $this->first + 1).' requested, '. |
||
| 1070 | $this->headersBlackListed.' blacklisted, '.$this->notYEnc.' not yEnc.' |
||
| 1071 | ), true |
||
| 1072 | ); |
||
| 1073 | } |
||
| 1074 | |||
| 1075 | /** |
||
| 1076 | * Outputs speed metrics of the scan function to CLI. |
||
| 1077 | */ |
||
| 1078 | protected function outputHeaderDuration(): void |
||
| 1079 | { |
||
| 1080 | $currentMicroTime = microtime(true); |
||
| 1081 | if ($this->_echoCLI) { |
||
| 1082 | ColorCLI::doEcho( |
||
| 1083 | ColorCLI::alternateOver($this->timeHeaders.'s'). |
||
| 1084 | ColorCLI::primaryOver(' to download articles, '). |
||
| 1085 | ColorCLI::alternateOver($this->timeCleaning.'s'). |
||
| 1086 | ColorCLI::primaryOver(' to process collections, '). |
||
| 1087 | ColorCLI::alternateOver($this->timeInsert.'s'). |
||
| 1088 | ColorCLI::primaryOver(' to insert binaries/parts, '). |
||
| 1089 | ColorCLI::alternateOver(number_format($currentMicroTime - $this->startPR, 2).'s'). |
||
| 1090 | ColorCLI::primaryOver(' for part repair, '). |
||
| 1091 | ColorCLI::alternateOver(number_format($currentMicroTime - $this->startLoop, 2).'s'). |
||
| 1092 | ColorCLI::primary(' total.'), true); |
||
| 1093 | } |
||
| 1094 | } |
||
| 1095 | |||
| 1096 | /** |
||
| 1097 | * If we failed to insert Collections/Binaries/Parts, rollback the transaction and add the parts to part repair. |
||
| 1098 | * |
||
| 1099 | * @param array $headers Array of headers containing sub-arrays with parts. |
||
| 1100 | * |
||
| 1101 | * @return array Array of article numbers to add to part repair. |
||
| 1102 | */ |
||
| 1103 | protected function _rollbackAddToPartRepair(array $headers): array |
||
| 1104 | { |
||
| 1105 | $headersNotInserted = []; |
||
| 1106 | foreach ($headers as $header) { |
||
| 1107 | foreach ($header as $file) { |
||
| 1108 | $headersNotInserted[] = $file['Parts']['number']; |
||
| 1109 | } |
||
| 1110 | } |
||
| 1111 | $this->_pdo->Rollback(); |
||
| 1112 | |||
| 1113 | return $headersNotInserted; |
||
| 1114 | } |
||
| 1115 | |||
| 1116 | /** |
||
| 1117 | * Attempt to get missing article headers. |
||
| 1118 | * |
||
| 1119 | * @param array|string $tables |
||
| 1120 | * @param array $groupArr The info for this group from mysql. |
||
| 1121 | * |
||
| 1122 | * @return void |
||
| 1123 | * @throws \Exception |
||
| 1124 | */ |
||
| 1125 | public function partRepair($groupArr, $tables = ''): void |
||
| 1126 | { |
||
| 1127 | $tableNames = $tables; |
||
| 1128 | |||
| 1129 | if ($tableNames === '') { |
||
| 1130 | $tableNames = (new Group())->getCBPTableNames($groupArr['id']); |
||
| 1131 | } |
||
| 1132 | // Get all parts in partrepair table. |
||
| 1133 | $missingParts = $this->_pdo->query( |
||
| 1134 | sprintf( |
||
| 1135 | ' |
||
| 1136 | SELECT * FROM %s |
||
| 1137 | WHERE groups_id = %d AND attempts < %d |
||
| 1138 | ORDER BY numberid ASC LIMIT %d', |
||
| 1139 | $tableNames['prname'], |
||
| 1140 | $groupArr['id'], |
||
| 1141 | $this->_partRepairMaxTries, |
||
| 1142 | $this->_partRepairLimit |
||
| 1143 | ) |
||
| 1144 | ); |
||
| 1145 | |||
| 1146 | $missingCount = \count($missingParts); |
||
| 1147 | if ($missingCount > 0) { |
||
| 1148 | if ($this->_echoCLI) { |
||
| 1149 | ColorCLI::doEcho( |
||
| 1150 | ColorCLI::primary( |
||
| 1151 | 'Attempting to repair '. |
||
| 1152 | number_format($missingCount). |
||
| 1153 | ' parts.' |
||
| 1154 | ), |
||
| 1155 | true |
||
| 1156 | ); |
||
| 1157 | } |
||
| 1158 | |||
| 1159 | // Loop through each part to group into continuous ranges with a maximum range of messagebuffer/4. |
||
| 1160 | $ranges = $partList = []; |
||
| 1161 | $firstPart = $lastNum = $missingParts[0]['numberid']; |
||
| 1162 | |||
| 1163 | foreach ($missingParts as $part) { |
||
| 1164 | if (($part['numberid'] - $firstPart) > ($this->messageBuffer / 4)) { |
||
| 1165 | $ranges[] = [ |
||
| 1166 | 'partfrom' => $firstPart, |
||
| 1167 | 'partto' => $lastNum, |
||
| 1168 | 'partlist' => $partList, |
||
| 1169 | ]; |
||
| 1170 | |||
| 1171 | $firstPart = $part['numberid']; |
||
| 1172 | $partList = []; |
||
| 1173 | } |
||
| 1174 | $partList[] = $part['numberid']; |
||
| 1175 | $lastNum = $part['numberid']; |
||
| 1176 | } |
||
| 1177 | |||
| 1178 | $ranges[] = [ |
||
| 1179 | 'partfrom' => $firstPart, |
||
| 1180 | 'partto' => $lastNum, |
||
| 1181 | 'partlist' => $partList, |
||
| 1182 | ]; |
||
| 1183 | |||
| 1184 | // Download missing parts in ranges. |
||
| 1185 | foreach ($ranges as $range) { |
||
| 1186 | $partFrom = $range['partfrom']; |
||
| 1187 | $partTo = $range['partto']; |
||
| 1188 | $partList = $range['partlist']; |
||
| 1189 | |||
| 1190 | if ($this->_echoCLI) { |
||
| 1191 | echo \chr(random_int(45, 46)).PHP_EOL; |
||
| 1192 | } |
||
| 1193 | |||
| 1194 | // Get article headers from newsgroup. |
||
| 1195 | $this->scan($groupArr, $partFrom, $partTo, 'missed_parts', $partList); |
||
| 1196 | } |
||
| 1197 | |||
| 1198 | // Calculate parts repaired |
||
| 1199 | $result = $this->_pdo->queryOneRow( |
||
| 1200 | sprintf( |
||
| 1201 | ' |
||
| 1202 | SELECT COUNT(id) AS num |
||
| 1203 | FROM %s |
||
| 1204 | WHERE groups_id = %d |
||
| 1205 | AND numberid <= %d', |
||
| 1206 | $tableNames['prname'], |
||
| 1207 | $groupArr['id'], |
||
| 1208 | $missingParts[$missingCount - 1]['numberid'] |
||
| 1209 | ) |
||
| 1210 | ); |
||
| 1211 | |||
| 1212 | $partsRepaired = 0; |
||
| 1213 | if ($result !== false) { |
||
| 1214 | $partsRepaired = ($missingCount - $result['num']); |
||
| 1215 | } |
||
| 1216 | |||
| 1217 | // Update attempts on remaining parts for active group |
||
| 1218 | if (isset($missingParts[$missingCount - 1]['id'])) { |
||
| 1219 | $this->_pdo->queryExec( |
||
| 1220 | sprintf( |
||
| 1221 | ' |
||
| 1222 | UPDATE %s |
||
| 1223 | SET attempts = attempts + 1 |
||
| 1224 | WHERE groups_id = %d |
||
| 1225 | AND numberid <= %d', |
||
| 1226 | $tableNames['prname'], |
||
| 1227 | $groupArr['id'], |
||
| 1228 | $missingParts[$missingCount - 1]['numberid'] |
||
| 1229 | ) |
||
| 1230 | ); |
||
| 1231 | } |
||
| 1232 | |||
| 1233 | if ($this->_echoCLI) { |
||
| 1234 | ColorCLI::doEcho( |
||
| 1235 | ColorCLI::primary( |
||
| 1236 | PHP_EOL. |
||
| 1237 | number_format($partsRepaired). |
||
| 1238 | ' parts repaired.' |
||
| 1239 | ), |
||
| 1240 | true |
||
| 1241 | ); |
||
| 1242 | } |
||
| 1243 | } |
||
| 1244 | |||
| 1245 | // Remove articles that we cant fetch after x attempts. |
||
| 1246 | $this->_pdo->queryExec( |
||
| 1247 | sprintf( |
||
| 1248 | 'DELETE FROM %s WHERE attempts >= %d AND groups_id = %d', |
||
| 1249 | $tableNames['prname'], |
||
| 1250 | $this->_partRepairMaxTries, |
||
| 1251 | $groupArr['id'] |
||
| 1252 | ) |
||
| 1253 | ); |
||
| 1254 | } |
||
| 1255 | |||
| 1256 | /** |
||
| 1257 | * Returns unix time for an article number. |
||
| 1258 | * |
||
| 1259 | * @param int $post The article number to get the time from. |
||
| 1260 | * @param array $groupData Usenet group info from NNTP selectGroup method. |
||
| 1261 | * |
||
| 1262 | * @return int Timestamp. |
||
| 1263 | * @throws \Exception |
||
| 1264 | */ |
||
| 1265 | public function postdate($post, array $groupData): int |
||
| 1266 | { |
||
| 1267 | // Set table names |
||
| 1268 | $groupID = Group::getIDByName($groupData['group']); |
||
| 1269 | $group = []; |
||
| 1270 | if ($groupID !== '') { |
||
| 1271 | $group = (new Group())->getCBPTableNames($groupID); |
||
| 1272 | } |
||
| 1273 | |||
| 1274 | $currentPost = $post; |
||
| 1275 | |||
| 1276 | $attempts = $date = 0; |
||
| 1277 | do { |
||
| 1278 | // Try to get the article date locally first. |
||
| 1279 | if ($groupID !== '') { |
||
| 1280 | // Try to get locally. |
||
| 1281 | $local = $this->_pdo->queryOneRow( |
||
| 1282 | sprintf( |
||
| 1283 | ' |
||
| 1284 | SELECT c.date AS date |
||
| 1285 | FROM %s c |
||
| 1286 | INNER JOIN %s b ON(c.id=b.collections_id) |
||
| 1287 | INNER JOIN %s p ON(b.id=p.binaries_id) |
||
| 1288 | WHERE p.number = %s', |
||
| 1289 | $group['cname'], |
||
| 1290 | $group['bname'], |
||
| 1291 | $group['pname'], |
||
| 1292 | $currentPost |
||
| 1293 | ) |
||
| 1294 | ); |
||
| 1295 | if ($local !== false) { |
||
| 1296 | $date = $local['date']; |
||
| 1297 | break; |
||
| 1298 | } |
||
| 1299 | } |
||
| 1300 | |||
| 1301 | // If we could not find it locally, try usenet. |
||
| 1302 | $header = $this->_nntp->getXOVER($currentPost); |
||
| 1303 | if (! $this->_nntp->isError($header)) { |
||
| 1304 | // Check if the date is set. |
||
| 1305 | if (isset($header[0]['Date']) && \strlen($header[0]['Date']) > 0) { |
||
| 1306 | $date = $header[0]['Date']; |
||
| 1307 | break; |
||
| 1308 | } |
||
| 1309 | } |
||
| 1310 | |||
| 1311 | // Try to get a different article number. |
||
| 1312 | if (abs($currentPost - $groupData['first']) > abs($groupData['last'] - $currentPost)) { |
||
| 1313 | $tempPost = round($currentPost / (random_int(1005, 1012) / 1000), 0, PHP_ROUND_HALF_UP); |
||
| 1314 | if ($tempPost < $groupData['first']) { |
||
| 1315 | $tempPost = $groupData['first']; |
||
| 1316 | } |
||
| 1317 | } else { |
||
| 1318 | $tempPost = round((random_int(1005, 1012) / 1000) * $currentPost, 0, PHP_ROUND_HALF_UP); |
||
| 1319 | if ($tempPost > $groupData['last']) { |
||
| 1320 | $tempPost = $groupData['last']; |
||
| 1321 | } |
||
| 1322 | } |
||
| 1323 | // If we got the same article number as last time, give up. |
||
| 1324 | if ($tempPost === $currentPost) { |
||
| 1325 | break; |
||
| 1326 | } |
||
| 1327 | $currentPost = $tempPost; |
||
| 1328 | |||
| 1329 | if ($this->_debug) { |
||
| 1330 | ColorCLI::doEcho(ColorCLI::debug('Postdate retried '.$attempts.' time(s).'), true); |
||
| 1331 | } |
||
| 1332 | } while ($attempts++ <= 20); |
||
| 1333 | |||
| 1334 | // If we didn't get a date, set it to now. |
||
| 1335 | if (! $date) { |
||
| 1336 | $date = time(); |
||
| 1337 | } else { |
||
| 1338 | $date = strtotime($date); |
||
| 1339 | } |
||
| 1340 | |||
| 1341 | return $date; |
||
| 1342 | } |
||
| 1343 | |||
| 1344 | /** |
||
| 1345 | * Returns article number based on # of days. |
||
| 1346 | * |
||
| 1347 | * @param int $days How many days back we want to go. |
||
| 1348 | * @param array $data Group data from usenet. |
||
| 1349 | * |
||
| 1350 | * @return string |
||
| 1351 | * @throws \Exception |
||
| 1352 | */ |
||
| 1353 | public function daytopost($days, $data): string |
||
| 1354 | { |
||
| 1355 | $goalTime = Carbon::now()->subDays($days)->timestamp; |
||
| 1356 | // The time we want = current unix time (ex. 1395699114) - minus 86400 (seconds in a day) |
||
| 1357 | // times days wanted. (ie 1395699114 - 2592000 (30days)) = 1393107114 |
||
| 1358 | |||
| 1359 | // The servers oldest date. |
||
| 1360 | $firstDate = $this->postdate($data['first'], $data); |
||
| 1361 | if ($goalTime < $firstDate) { |
||
| 1362 | // If the date we want is older than the oldest date in the group return the groups oldest article. |
||
| 1363 | return $data['first']; |
||
| 1364 | } |
||
| 1365 | |||
| 1366 | // The servers newest date. |
||
| 1367 | $lastDate = $this->postdate($data['last'], $data); |
||
| 1368 | if ($goalTime > $lastDate) { |
||
| 1369 | // If the date we want is newer than the groups newest date, return the groups newest article. |
||
| 1370 | return $data['last']; |
||
| 1371 | } |
||
| 1372 | |||
| 1373 | if ($this->_echoCLI) { |
||
| 1374 | ColorCLI::doEcho( |
||
| 1375 | ColorCLI::primary( |
||
| 1376 | 'Searching for an approximate article number for group '.$data['group'].' '.$days.' days back.' |
||
| 1377 | ), true |
||
| 1378 | ); |
||
| 1379 | } |
||
| 1380 | |||
| 1381 | // Pick the middle to start with |
||
| 1382 | $wantedArticle = round(($data['last'] + $data['first']) / 2); |
||
| 1383 | $aMax = $data['last']; |
||
| 1384 | $aMin = $data['first']; |
||
| 1385 | $reallyOldArticle = $oldArticle = $articleTime = null; |
||
| 1386 | |||
| 1387 | while (true) { |
||
| 1388 | // Article exists outside of available range, this shouldn't happen |
||
| 1389 | if ($wantedArticle <= $data['first'] || $wantedArticle >= $data['last']) { |
||
| 1390 | break; |
||
| 1391 | } |
||
| 1392 | |||
| 1393 | // Keep a note of the last articles we checked |
||
| 1394 | $reallyOldArticle = $oldArticle; |
||
| 1395 | $oldArticle = $wantedArticle; |
||
| 1396 | |||
| 1397 | // Get the date of this article |
||
| 1398 | $articleTime = $this->postdate($wantedArticle, $data); |
||
| 1399 | |||
| 1400 | // Article doesn't exist, start again with something random |
||
| 1401 | if (! $articleTime) { |
||
| 1402 | $wantedArticle = random_int($aMin, $aMax); |
||
| 1403 | $articleTime = $this->postdate($wantedArticle, $data); |
||
| 1404 | } |
||
| 1405 | |||
| 1406 | if ($articleTime < $goalTime) { |
||
| 1407 | // Article is older than we want |
||
| 1408 | $aMin = $oldArticle; |
||
| 1409 | $wantedArticle = round(($aMax + $oldArticle) / 2); |
||
| 1410 | if ($this->_echoCLI) { |
||
| 1411 | echo '-'; |
||
| 1412 | } |
||
| 1413 | } elseif ($articleTime > $goalTime) { |
||
| 1414 | // Article is newer than we want |
||
| 1415 | $aMax = $oldArticle; |
||
| 1416 | $wantedArticle = round(($aMin + $oldArticle) / 2); |
||
| 1417 | if ($this->_echoCLI) { |
||
| 1418 | echo '+'; |
||
| 1419 | } |
||
| 1420 | } elseif ($articleTime === $goalTime) { |
||
| 1421 | // Exact match. We did it! (this will likely never happen though) |
||
| 1422 | break; |
||
| 1423 | } |
||
| 1424 | |||
| 1425 | // We seem to be flip-flopping between 2 articles, assume we're out of articles to check. |
||
| 1426 | // End on an article more recent than our oldest so that we don't miss any releases. |
||
| 1427 | if ($reallyOldArticle === $wantedArticle && ($goalTime - $articleTime) <= 0) { |
||
| 1428 | break; |
||
| 1429 | } |
||
| 1430 | } |
||
| 1431 | |||
| 1432 | $wantedArticle = (int) $wantedArticle; |
||
| 1433 | if ($this->_echoCLI) { |
||
| 1434 | ColorCLI::doEcho( |
||
| 1435 | ColorCLI::primary( |
||
| 1436 | PHP_EOL.'Found article #'.$wantedArticle.' which has a date of '.date('r', $articleTime). |
||
| 1437 | ', vs wanted date of '.date('r', $goalTime).'. Difference from goal is '.Carbon::createFromTimestamp($goalTime)->diffInDays(Carbon::createFromTimestamp($articleTime)).'days.' |
||
| 1438 | ), true |
||
| 1439 | ); |
||
| 1440 | } |
||
| 1441 | |||
| 1442 | return $wantedArticle; |
||
| 1443 | } |
||
| 1444 | |||
| 1445 | /** |
||
| 1446 | * Convert unix time to days ago. |
||
| 1447 | * |
||
| 1448 | * |
||
| 1449 | * @param $timestamp |
||
| 1450 | * @return int |
||
| 1451 | */ |
||
| 1452 | private function daysOld($timestamp): int |
||
| 1453 | { |
||
| 1454 | return Carbon::createFromTimestamp($timestamp)->diffInDays(); |
||
| 1455 | } |
||
| 1456 | |||
| 1457 | /** |
||
| 1458 | * Add article numbers from missing headers to DB. |
||
| 1459 | * |
||
| 1460 | * @param array $numbers The article numbers of the missing headers. |
||
| 1461 | * @param string $tableName Name of the partrepair table to insert into. |
||
| 1462 | * @param int $groupID The ID of this groups. |
||
| 1463 | * |
||
| 1464 | * @return bool |
||
| 1465 | */ |
||
| 1466 | private function addMissingParts($numbers, $tableName, $groupID): bool |
||
| 1467 | { |
||
| 1468 | $insertStr = 'INSERT INTO '.$tableName.' (numberid, groups_id) VALUES '; |
||
| 1469 | foreach ($numbers as $number) { |
||
| 1470 | $insertStr .= '('.$number.','.$groupID.'),'; |
||
| 1471 | } |
||
| 1472 | |||
| 1473 | return $this->_pdo->queryInsert(rtrim($insertStr, ',').' ON DUPLICATE KEY UPDATE attempts=attempts+1'); |
||
| 1474 | } |
||
| 1475 | |||
| 1476 | /** |
||
| 1477 | * Clean up part repair table. |
||
| 1478 | * |
||
| 1479 | * @param array $numbers The article numbers. |
||
| 1480 | * @param string $tableName Name of the part repair table to work on. |
||
| 1481 | * @param int $groupID The ID of the group. |
||
| 1482 | * |
||
| 1483 | * @return void |
||
| 1484 | */ |
||
| 1485 | private function removeRepairedParts(array $numbers, $tableName, $groupID): void |
||
| 1486 | { |
||
| 1487 | $sql = 'DELETE FROM '.$tableName.' WHERE numberid in ('; |
||
| 1488 | foreach ($numbers as $number) { |
||
| 1489 | $sql .= $number.','; |
||
| 1490 | } |
||
| 1491 | $this->_pdo->queryExec(rtrim($sql, ',').') AND groups_id = '.$groupID); |
||
| 1492 | } |
||
| 1493 | |||
| 1494 | /** |
||
| 1495 | * Are white or black lists loaded for a group name? |
||
| 1496 | * @var array |
||
| 1497 | */ |
||
| 1498 | protected $_listsFound = []; |
||
| 1499 | |||
| 1500 | /** |
||
| 1501 | * Get blacklist and cache it. Return if already cached. |
||
| 1502 | * |
||
| 1503 | * @param string $groupName |
||
| 1504 | * |
||
| 1505 | * @return void |
||
| 1506 | */ |
||
| 1507 | protected function _retrieveBlackList($groupName): void |
||
| 1508 | { |
||
| 1509 | if (! isset($this->blackList[$groupName])) { |
||
| 1510 | $this->blackList[$groupName] = $this->getBlacklist(true, self::OPTYPE_BLACKLIST, $groupName, true); |
||
| 1511 | } |
||
| 1512 | if (! isset($this->whiteList[$groupName])) { |
||
| 1513 | $this->whiteList[$groupName] = $this->getBlacklist(true, self::OPTYPE_WHITELIST, $groupName, true); |
||
| 1514 | } |
||
| 1515 | $this->_listsFound[$groupName] = ($this->blackList[$groupName] || $this->whiteList[$groupName]); |
||
| 1516 | } |
||
| 1517 | |||
| 1518 | /** |
||
| 1519 | * Check if an article is blacklisted. |
||
| 1520 | * |
||
| 1521 | * @param array $msg The article header (OVER format). |
||
| 1522 | * @param string $groupName The group name. |
||
| 1523 | * |
||
| 1524 | * @return bool |
||
| 1525 | */ |
||
| 1526 | public function isBlackListed($msg, $groupName): bool |
||
| 1527 | { |
||
| 1528 | if (! isset($this->_listsFound[$groupName])) { |
||
| 1529 | $this->_retrieveBlackList($groupName); |
||
| 1530 | } |
||
| 1531 | if (! $this->_listsFound[$groupName]) { |
||
| 1532 | return false; |
||
| 1533 | } |
||
| 1534 | |||
| 1535 | $blackListed = false; |
||
| 1536 | |||
| 1537 | $field = [ |
||
| 1538 | self::BLACKLIST_FIELD_SUBJECT => $msg['Subject'], |
||
| 1539 | self::BLACKLIST_FIELD_FROM => $msg['From'], |
||
| 1540 | self::BLACKLIST_FIELD_MESSAGEID => $msg['Message-ID'], |
||
| 1541 | ]; |
||
| 1542 | |||
| 1543 | // Try white lists first. |
||
| 1544 | if ($this->whiteList[$groupName]) { |
||
| 1545 | // There are white lists for this group, so anything that doesn't match a white list should be considered black listed. |
||
| 1546 | $blackListed = true; |
||
| 1547 | foreach ($this->whiteList[$groupName] as $whiteList) { |
||
| 1548 | if (preg_match('/'.$whiteList['regex'].'/i', $field[$whiteList['msgcol']])) { |
||
| 1549 | // This field matched a white list, so it might not be black listed. |
||
| 1550 | $blackListed = false; |
||
| 1551 | $this->_binaryBlacklistIdsToUpdate[$whiteList['id']] = $whiteList['id']; |
||
| 1552 | break; |
||
| 1553 | } |
||
| 1554 | } |
||
| 1555 | } |
||
| 1556 | |||
| 1557 | // Check if the field is black listed. |
||
| 1558 | if (! $blackListed && $this->blackList[$groupName]) { |
||
| 1559 | foreach ($this->blackList[$groupName] as $blackList) { |
||
| 1560 | if (preg_match('/'.$blackList['regex'].'/i', $field[$blackList['msgcol']])) { |
||
| 1561 | $blackListed = true; |
||
| 1562 | $this->_binaryBlacklistIdsToUpdate[$blackList['id']] = $blackList['id']; |
||
| 1563 | break; |
||
| 1564 | } |
||
| 1565 | } |
||
| 1566 | } |
||
| 1567 | |||
| 1568 | return $blackListed; |
||
| 1569 | } |
||
| 1570 | |||
| 1571 | /** |
||
| 1572 | * Return all blacklists. |
||
| 1573 | * |
||
| 1574 | * @param bool $activeOnly Only display active blacklists ? |
||
| 1575 | * @param int|string $opType Optional, get white or black lists (use Binaries constants). |
||
| 1576 | * @param string $groupName Optional, group. |
||
| 1577 | * @param bool $groupRegex Optional Join groups / binaryblacklist using regexp for equals. |
||
| 1578 | * |
||
| 1579 | * @return array |
||
| 1580 | */ |
||
| 1581 | public function getBlacklist($activeOnly = true, $opType = -1, $groupName = '', $groupRegex = false): array |
||
| 1582 | { |
||
| 1583 | switch ($opType) { |
||
| 1584 | case self::OPTYPE_BLACKLIST: |
||
| 1585 | $opType = 'AND bb.optype = '.self::OPTYPE_BLACKLIST; |
||
| 1586 | break; |
||
| 1587 | case self::OPTYPE_WHITELIST: |
||
| 1588 | $opType = 'AND bb.optype = '.self::OPTYPE_WHITELIST; |
||
| 1589 | break; |
||
| 1590 | default: |
||
| 1591 | $opType = ''; |
||
| 1592 | break; |
||
| 1593 | } |
||
| 1594 | |||
| 1595 | return $this->_pdo->query( |
||
| 1596 | sprintf( |
||
| 1597 | ' |
||
| 1598 | SELECT |
||
| 1599 | bb.id, bb.optype, bb.status, bb.description, |
||
| 1600 | bb.groupname AS groupname, bb.regex, g.id AS group_id, bb.msgcol, |
||
| 1601 | bb.last_activity as last_activity |
||
| 1602 | FROM binaryblacklist bb |
||
| 1603 | LEFT OUTER JOIN groups g ON g.name %s bb.groupname |
||
| 1604 | WHERE 1=1 %s %s %s |
||
| 1605 | ORDER BY coalesce(groupname,\'zzz\')', |
||
| 1606 | ($groupRegex ? 'REGEXP' : '='), |
||
| 1607 | ($activeOnly ? 'AND bb.status = 1' : ''), |
||
| 1608 | $opType, |
||
| 1609 | ($groupName ? ('AND g.name REGEXP '.$this->_pdo->escapeString($groupName)) : '') |
||
| 1610 | ) |
||
| 1611 | ); |
||
| 1612 | } |
||
| 1613 | |||
| 1614 | /** |
||
| 1615 | * Return the specified blacklist. |
||
| 1616 | * |
||
| 1617 | * @param int $id The blacklist ID. |
||
| 1618 | * |
||
| 1619 | * @return \Illuminate\Database\Eloquent\Model|null|static |
||
| 1620 | */ |
||
| 1621 | public function getBlacklistByID($id) |
||
| 1624 | } |
||
| 1625 | |||
| 1626 | /** |
||
| 1627 | * Delete a blacklist. |
||
| 1628 | * |
||
| 1629 | * @param int $id The ID of the blacklist. |
||
| 1630 | */ |
||
| 1631 | public function deleteBlacklist($id): void |
||
| 1632 | { |
||
| 1633 | BinaryBlacklist::query()->where('id', $id)->delete(); |
||
| 1634 | } |
||
| 1635 | |||
| 1636 | /** |
||
| 1637 | * @param $blacklistArray |
||
| 1638 | */ |
||
| 1639 | public function updateBlacklist($blacklistArray): void |
||
| 1640 | { |
||
| 1641 | BinaryBlacklist::query()->where('id', $blacklistArray['id'])->update( |
||
| 1642 | [ |
||
| 1643 | 'groupname' => $blacklistArray['groupname'] === '' ? 'null' : preg_replace('/a\.b\./i', 'alt.binaries.', $blacklistArray['groupname']), |
||
| 1644 | 'regex' => $blacklistArray['regex'], |
||
| 1645 | 'status' => $blacklistArray['status'], |
||
| 1646 | 'description' => $blacklistArray['description'], |
||
| 1647 | 'optype' => $blacklistArray['optype'], |
||
| 1648 | 'msgcol' => $blacklistArray['msgcol'], |
||
| 1649 | ] |
||
| 1650 | ); |
||
| 1651 | } |
||
| 1652 | |||
| 1653 | /** |
||
| 1654 | * Adds a new blacklist from binary blacklist edit admin web page. |
||
| 1655 | * |
||
| 1656 | * @param array $blacklistArray |
||
| 1657 | */ |
||
| 1658 | public function addBlacklist($blacklistArray): void |
||
| 1659 | { |
||
| 1660 | BinaryBlacklist::query()->insert( |
||
| 1661 | [ |
||
| 1662 | 'groupname' => $blacklistArray['groupname'] === '' ? 'null' : preg_replace('/a\.b\./i', 'alt.binaries.', $blacklistArray['groupname']), |
||
| 1663 | 'regex' => $blacklistArray['regex'], |
||
| 1664 | 'status' => $blacklistArray['status'], |
||
| 1665 | 'description' => $blacklistArray['description'], |
||
| 1666 | 'optype' => $blacklistArray['optype'], |
||
| 1667 | 'msgcol' => $blacklistArray['msgcol'], |
||
| 1668 | ] |
||
| 1669 | ); |
||
| 1670 | } |
||
| 1671 | |||
| 1672 | /** |
||
| 1673 | * Delete Collections/Binaries/Parts for a Collection ID. |
||
| 1674 | * |
||
| 1675 | * @param int $collectionID Collections table ID |
||
| 1676 | * |
||
| 1677 | * @note A trigger automatically deletes the parts/binaries. |
||
| 1678 | * |
||
| 1679 | * @return void |
||
| 1680 | */ |
||
| 1681 | public function delete($collectionID): void |
||
| 1682 | { |
||
| 1683 | $this->_pdo->queryExec(sprintf('DELETE FROM collections WHERE id = %d', $collectionID)); |
||
| 1684 | } |
||
| 1685 | |||
| 1686 | /** |
||
| 1687 | * Log / Echo message. |
||
| 1688 | * |
||
| 1689 | * @param string $message Message to log. |
||
| 1690 | * @param string $method Method that called this. |
||
| 1691 | * @param string $color ColorCLI method name. |
||
| 1692 | */ |
||
| 1693 | private function log($message, $method, $color): void |
||
| 1699 | ); |
||
| 1700 | } |
||
| 1701 | } |
||
| 1702 | |||
| 1703 | /** |
||
| 1704 | * Check if we should ignore the file count and return true or false. |
||
| 1705 | * |
||
| 1706 | * @param string $groupName |
||
| 1707 | * @param string $subject |
||
| 1708 | * |
||
| 1709 | * @return bool |
||
| 1710 | */ |
||
| 1711 | protected function _ignoreFileCount($groupName, $subject): bool |
||
| 1712 | { |
||
| 1713 | $ignore = false; |
||
| 1714 | switch ($groupName) { |
||
| 1715 | case 'alt.binaries.erotica': |
||
| 1716 | if (preg_match('/^\[\d+\]-\[FULL\]-\[#a\.b\.erotica@EFNet\]-\[ \d{2,3}_/', $subject)) { |
||
| 1717 | $ignore = true; |
||
| 1718 | } |
||
| 1719 | break; |
||
| 1720 | } |
||
| 1721 | |||
| 1722 | return $ignore; |
||
| 1723 | } |
||
| 1724 | |||
| 1725 | /** |
||
| 1726 | * Returns all multigroup poster entries from the database. |
||
| 1727 | * |
||
| 1728 | * |
||
| 1729 | * @return \Illuminate\Database\Eloquent\Collection|static[] |
||
| 1730 | */ |
||
| 1731 | protected function getMultiGroupPosters() |
||
| 1743 | } |
||
| 1744 | } |
||
| 1745 |