2022-11-01 11:24:36 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace App\Classes\FTN;
|
|
|
|
|
|
|
|
use Carbon\Carbon;
|
|
|
|
use Illuminate\Contracts\Filesystem\FileNotFoundException;
|
|
|
|
use Illuminate\Support\Facades\Log;
|
2022-11-02 10:20:02 +00:00
|
|
|
use Illuminate\Support\Facades\Storage;
|
2022-11-01 11:24:36 +00:00
|
|
|
use League\Flysystem\UnableToWriteFile;
|
|
|
|
|
|
|
|
use App\Classes\FTN as FTNBase;
|
2022-11-15 11:01:05 +00:00
|
|
|
use App\Models\{Address,File,Filearea};
|
2022-11-01 11:24:36 +00:00
|
|
|
use App\Traits\EncodeUTF8;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Class TIC
|
|
|
|
*
|
|
|
|
* @package App\Classes
|
|
|
|
*/
|
|
|
|
class Tic extends FTNBase
|
|
|
|
{
|
|
|
|
use EncodeUTF8;
|
|
|
|
|
2022-11-15 11:01:05 +00:00
|
|
|
private const LOGKEY = 'FT-';
|
2022-11-01 11:24:36 +00:00
|
|
|
|
|
|
|
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 $file;
|
|
|
|
private Filearea $area;
|
|
|
|
private ?string $areadesc = NULL;
|
|
|
|
private ?string $pw = NULL;
|
|
|
|
|
|
|
|
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(private string $filename) {
|
2022-11-03 11:05:49 +00:00
|
|
|
Log::info(sprintf('%s:Processing TIC file [%s]',self::LOGKEY,$filename));
|
|
|
|
|
2022-11-01 11:24:36 +00:00
|
|
|
$fo = new File;
|
|
|
|
|
|
|
|
$fo->kludges = collect();
|
|
|
|
$fo->set_path = collect();
|
|
|
|
$fo->set_seenby = collect();
|
|
|
|
$fo->rogue_path = collect();
|
|
|
|
$fo->rogue_seenby = collect();
|
|
|
|
|
|
|
|
list($hex,$name) = explode('-',$filename);
|
2022-11-03 11:05:49 +00:00
|
|
|
$hex = basename($hex);
|
2022-11-01 11:24:36 +00:00
|
|
|
|
2022-11-03 11:05:49 +00:00
|
|
|
if (! file_exists($filename))
|
|
|
|
throw new FileNotFoundException(sprintf('%s:File [%s] doesnt exist',self::LOGKEY,realpath($filename)));
|
2022-11-01 11:24:36 +00:00
|
|
|
|
2022-11-03 11:05:49 +00:00
|
|
|
if (! is_writable($filename))
|
|
|
|
throw new UnableToWriteFile(sprintf('%s:File [%s] is not writable',self::LOGKEY,realpath($filename)));
|
2022-11-01 11:24:36 +00:00
|
|
|
|
2022-11-03 11:05:49 +00:00
|
|
|
$f = fopen($filename,'rb');
|
2022-11-01 11:24:36 +00:00
|
|
|
if (! $f) {
|
2022-11-03 11:05:49 +00:00
|
|
|
Log::error(sprintf('%s:! Unable to open file [%s] for writing',self::LOGKEY,$filename));
|
2022-11-01 11:24:36 +00:00
|
|
|
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':
|
2022-11-02 10:20:02 +00:00
|
|
|
if (! Storage::disk('local')->exists($x=sprintf('%s/%s-%s',config('app.fido'),$hex,$matches[2])))
|
2022-11-01 11:24:36 +00:00
|
|
|
throw new FileNotFoundException(sprintf('File not found? [%s]',$x));
|
|
|
|
|
|
|
|
$fo->{$k} = $matches[2];
|
|
|
|
$fo->fullname = $x;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'areadesc':
|
|
|
|
case 'pw':
|
|
|
|
case 'created':
|
|
|
|
$this->{$k} = $matches[2];
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'lfile':
|
|
|
|
case 'size':
|
|
|
|
case 'desc':
|
|
|
|
case 'magic':
|
|
|
|
case 'replaces':
|
|
|
|
$fo->{$k} = $matches[2];
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'fullname':
|
|
|
|
$fo->lfile = $matches[2];
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'date':
|
|
|
|
$fo->datetime = Carbon::create($matches[2]);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'ldesc':
|
|
|
|
$fo->{$k} .= $matches[2];
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'crc':
|
|
|
|
$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) {
|
|
|
|
$fo->rogue_path->push($matches[2]);
|
|
|
|
} else {
|
|
|
|
$fo->set_path->push(['address'=>$ao,'datetime'=>Carbon::createFromTimestamp($x[8]),'extra'=>$x[9]]);
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 'seenby':
|
|
|
|
$ao = Address::findFTN($matches[2]);
|
|
|
|
|
|
|
|
if (! $ao) {
|
|
|
|
$fo->rogue_seenby->push($matches[2]);
|
|
|
|
} else {
|
|
|
|
$fo->set_seenby->push($ao->id);
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
$fo->kludges->push($line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fclose($f);
|
|
|
|
|
2022-11-02 10:20:02 +00:00
|
|
|
$f = fopen($x=Storage::disk('local')->path($fo->fullname),'rb');
|
2022-11-01 11:24:36 +00:00
|
|
|
$stat = fstat($f);
|
|
|
|
fclose($f);
|
|
|
|
|
|
|
|
// Validate Size
|
|
|
|
if ($fo->size !== ($y=$stat['size']))
|
|
|
|
throw new \Exception(sprintf('TIC file size [%d] doesnt match file [%s] (%d)',$fo->size,$fo->fullname,$y));
|
|
|
|
|
|
|
|
// Validate CRC
|
2022-11-11 13:15:45 +00:00
|
|
|
if (sprintf('%08x',$fo->crc) !== ($y=hash_file('crc32b',$x)))
|
|
|
|
throw new \Exception(sprintf('TIC file CRC [%08x] doesnt match file [%s] (%s)',$fo->crc,$fo->fullname,$y));
|
2022-11-01 11:24:36 +00:00
|
|
|
|
|
|
|
// Validate Password
|
|
|
|
if ($this->pw !== ($y=$this->from->session('ticpass')))
|
|
|
|
throw new \Exception(sprintf('TIC file PASSWORD [%s] doesnt match system [%s] (%s)',$this->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 = $this->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();
|
|
|
|
}
|
|
|
|
|
|
|
|
$fo->filearea_id = $this->area->id;
|
|
|
|
$fo->fftn_id = $this->origin->id;
|
|
|
|
|
|
|
|
// If the file create time is blank, we'll take the files
|
|
|
|
if (! $fo->datetime)
|
|
|
|
$fo->datetime = Carbon::createFromTimestamp($stat['ctime']);
|
|
|
|
|
|
|
|
$fo->save();
|
|
|
|
|
|
|
|
$this->fo = $fo;
|
|
|
|
}
|
|
|
|
|
2022-11-02 10:20:02 +00:00
|
|
|
public function isNodelist(): bool
|
2022-11-01 11:24:36 +00:00
|
|
|
{
|
2022-11-02 10:20:02 +00:00
|
|
|
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->file)));
|
2022-11-01 11:24:36 +00:00
|
|
|
}
|
|
|
|
}
|