clrghouz/app/Classes/FTN/Tic.php

291 lines
7.6 KiB
PHP

<?php
namespace App\Classes\FTN;
use Carbon\Carbon;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use League\Flysystem\UnableToWriteFile;
use App\Classes\FTN as FTNBase;
use App\Models\{Address,File,Filearea,Setup};
use App\Traits\EncodeUTF8;
/**
* Class TIC
* Used create the structure of TIC files
*
* @package App\Classes
*/
class Tic extends FTNBase
{
use EncodeUTF8;
private const LOGKEY = 'FT-';
private const cast_utf8 = [
];
// Single value kludge items and whether they are required
// http://ftsc.org/docs/fts-5006.001
private array $_kludge = [
'AREA' => TRUE,
'areadesc' => FALSE,
'ORIGIN' => TRUE,
'FROM' => TRUE,
'to' => FALSE,
'FILE' => TRUE, // 8.3 DOS format
'lfile' => FALSE, // alias fullname
'fullname' => FALSE,
'size' => FALSE,
'date' => FALSE, // File creation date
'desc' => FALSE, // One line description of file
'ldesc' => FALSE, // Can have multiple
'created' => FALSE,
'magic' => FALSE,
'replaces' => FALSE, // ? and * are wildcards, as per DOS
'CRC' => TRUE, // crc-32
'PATH' => TRUE, // can have multiple: [FTN] [unix timestamp] [datetime human readable] [signature]
'SEENBY' => TRUE,
'pw' => FALSE, // Password
];
private File $fo;
private Filearea $area;
private Collection $values;
private Address $origin; // Should be first address in Path
private Address $from; // Should be last address in Path
private Address $to; // Should be me
public function __construct()
{
$this->fo = new File;
$this->fo->kludges = collect();
$this->fo->set_path = collect();
$this->fo->set_seenby = collect();
$this->fo->rogue_path = collect();
$this->fo->rogue_seenby = collect();
$this->values = collect();
}
/**
* Does this TIC file bring us a nodelist
*
* @return bool
*/
public function isNodelist(): bool
{
return (($this->fo->nodelist_filearea_id === $this->fo->filearea->domain->filearea_id)
&& (preg_match(str_replace(['.','?'],['\.','.'],'#^'.$this->fo->filearea->domain->nodelist_filename.'$#i'),$this->fo->name)));
}
/**
* Generate a TIC file for an address
*
* @param Address $ao
* @param File $fo
* @return string
*/
public function generate(Address $ao,File $fo): string
{
$sysaddress = Setup::findOrFail(config('app.id'))->system->match($ao->zone)->first();
$result = collect();
// Origin is the first address in our path
$result->put('ORIGIN',$fo->path->first()->ftn3d);
$result->put('FROM',$sysaddress->ftn3d);
$result->put('TO',$ao->ftn3d);
$result->put('FILE',$fo->name);
$result->put('SIZE',$fo->size);
if ($fo->description)
$result->put('DESC',$fo->description);
$result->put('AREA',$fo->filearea->name);
$result->put('AREADESC',$fo->filearea->description);
$result->put('PW',$ao->session('ticpass'));
$result->put('CRC',sprintf("%X",$fo->crc));
$out = '';
foreach ($result as $key=>$value)
$out .= sprintf("%s %s\r\n",$key,$value);
foreach ($fo->path as $o)
$out .= sprintf("PATH %s %s %s\r\n",$o->ftn3d,$o->pivot->datetime,$o->pivot->extra);
foreach ($fo->seenby as $o)
$out .= sprintf("SEENBY %s\r\n",$o->ftn3d);
return $out;
}
/**
* Load a TIC file from an existing filename
*
* @param string $filename
* @return void
* @throws FileNotFoundException
*/
public function load(string $filename): void
{
Log::info(sprintf('%s:Processing TIC file [%s]',self::LOGKEY,$filename));
list($hex,$name) = explode('-',$filename);
$hex = basename($hex);
if (! file_exists($filename))
throw new FileNotFoundException(sprintf('%s:File [%s] doesnt exist',self::LOGKEY,realpath($filename)));
if (! is_readable($filename))
throw new UnableToWriteFile(sprintf('%s:File [%s] is not readable',self::LOGKEY,realpath($filename)));
$f = fopen($filename,'rb');
if (! $f) {
Log::error(sprintf('%s:! Unable to open file [%s] for writing',self::LOGKEY,$filename));
return;
}
while (! feof($f)) {
$line = chop(fgets($f));
$matches = [];
if (! $line)
continue;
preg_match('/([a-zA-Z]+)\ (.*)/',$line,$matches);
if (in_array(strtolower($matches[1]),$this->_kludge)) {
switch ($k=strtolower($matches[1])) {
case 'area':
$this->{$k} = Filearea::singleOrNew(['name'=>strtoupper($matches[2])]);
break;
case 'origin':
case 'from':
case 'to':
$this->{$k} = Address::findFTN($matches[2]);
// @todo If $this->{$k} is null, we have discovered the system and it should be created
break;
case 'file':
if (! Storage::disk('local')->exists($x=sprintf('%s/%s-%s',config('app.fido'),$hex,$matches[2])))
throw new FileNotFoundException(sprintf('File not found? [%s]',$x));
$this->fo->name = $matches[2];
$this->fo->fullname = $x;
break;
case 'areadesc':
$areadesc = $matches[2];
break;
case 'created':
// ignored
break;
case 'pw':
$pw = $matches[2];
case 'lfile':
$this->fo->lname = $matches[2];
case 'desc':
case 'magic':
case 'replaces':
case 'size':
$this->fo->{$k} = $matches[2];
break;
case 'fullname':
$this->fo->lfile = $matches[2];
break;
case 'date':
$this->fo->datetime = Carbon::create($matches[2]);
break;
case 'ldesc':
$this->fo->{$k} .= $matches[2];
break;
case 'crc':
$this->fo->{$k} = hexdec($matches[2]);
break;
case 'path':
$x = [];
preg_match(sprintf('#^[Pp]ath (%s)\ ?([0-9]+)\ ?(.*)$#',Address::ftn_regex),$line,$x);
$ao = Address::findFTN($x[1]);
if (! $ao) {
$this->fo->rogue_path->push($matches[2]);
} else {
$this->fo->set_path->push(['address'=>$ao,'datetime'=>Carbon::createFromTimestamp($x[8]),'extra'=>$x[9]]);
}
break;
case 'seenby':
$ao = Address::findFTN($matches[2]);
if (! $ao) {
$this->fo->rogue_seenby->push($matches[2]);
} else {
$this->fo->set_seenby->push($ao->id);
}
break;
}
} else {
$this->fo->kludges->push($line);
}
}
fclose($f);
$f = fopen($x=Storage::disk('local')->path($this->fo->fullname),'rb');
$stat = fstat($f);
fclose($f);
// Validate Size
if ($this->fo->size !== ($y=$stat['size']))
throw new \Exception(sprintf('TIC file size [%d] doesnt match file [%s] (%d)',$this->fo->size,$this->fo->fullname,$y));
// Validate CRC
if (sprintf('%08x',$this->fo->crc) !== ($y=hash_file('crc32b',$x)))
throw new \Exception(sprintf('TIC file CRC [%08x] doesnt match file [%s] (%s)',$this->fo->crc,$this->fo->fullname,$y));
// Validate Password
if ($pw !== ($y=$this->from->session('ticpass')))
throw new \Exception(sprintf('TIC file PASSWORD [%s] doesnt match system [%s] (%s)',$pw,$this->from->ftn,$y));
// Validate Sender is linked (and permitted to send)
if ($this->from->fileareas->search(function($item) { return $item->id === $this->area->id; }) === FALSE)
throw new \Exception(sprintf('Node [%s] is not subscribed to [%s]',$this->from->ftn,$this->area->name));
// If the filearea is to be autocreated, create it
if (! $this->area->exists) {
$this->area->description = $areadesc;
$this->area->active = TRUE;
$this->area->public = FALSE;
$this->area->notes = 'Autocreated';
$this->area->domain_id = $this->from->zone->domain_id;
$this->area->save();
}
$this->fo->filearea_id = $this->area->id;
$this->fo->fftn_id = $this->origin->id;
// If the file create time is blank, we'll take the files
if (! $this->fo->datetime)
$this->fo->datetime = Carbon::createFromTimestamp($stat['ctime']);
$this->fo->save();
}
}