Added file areas and TIC processing

This commit is contained in:
Deon George 2022-11-01 22:24:36 +11:00
parent 702c5fb4f2
commit 029a8a9d73
20 changed files with 908 additions and 35 deletions

227
app/Classes/FTN/Tic.php Normal file
View File

@ -0,0 +1,227 @@
<?php
namespace App\Classes\FTN;
use Carbon\Carbon;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use League\Flysystem\UnableToWriteFile;
use App\Classes\FTN as FTNBase;
use App\Models\{Address, File, Filearea, Setup};
use App\Traits\EncodeUTF8;
/**
* Class TIC
*
* @package App\Classes
*/
class Tic extends FTNBase
{
use EncodeUTF8;
private const LOGKEY = 'FM-';
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) {
$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);
$ticfullpath = $this->fullpath($filename);
if (! file_exists($ticfullpath))
throw new FileNotFoundException(sprintf('%s:File [%s] doesnt exist',self::LOGKEY,realpath($ticfullpath)));
if (! is_writable($ticfullpath))
throw new UnableToWriteFile(sprintf('%s:File [%s] is not writable',self::LOGKEY,realpath($ticfullpath)));
Log::info(sprintf('Processing TIC file [%s]',$ticfullpath));
$f = fopen($ticfullpath,'rb');
if (! $f) {
Log::error(sprintf('%s:! Unable to open file [%s] for writing',self::LOGKEY,$ticfullpath));
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 (! file_exists($x=$this->fullpath(sprintf('%s-%s',$hex,$matches[2]))))
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);
$f = fopen($fo->fullname,'rb');
$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
if (sprintf('%x',$fo->crc) !== ($y=hash_file('crc32b',$fo->fullname)))
throw new \Exception(sprintf('TIC file CRC [%x] doesnt match file [%s] (%s)',$fo->crc,$fo->fullname,$y));
// 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;
}
public function fullpath(string $file,string $prefix=NULL): string
{
return sprintf('storage/app/%s/%s',config('app.fido'),($prefix ? $prefix.'-' : '').$file);
}
}

View File

@ -4,7 +4,7 @@ namespace App\Classes\File;
use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use League\Flysystem\UnreadableFileException;
use League\Flysystem\UnreadableFileEncountered;
/**
* A file we are sending or receiving
@ -22,6 +22,7 @@ class Item
protected const IS_FILE = (1<<3);
protected const IS_FLO = (1<<4);
protected const IS_REQ = (1<<5);
protected const IS_TIC = (1<<6);
protected const I_RECV = (1<<6);
protected const I_SEND = (1<<7);
@ -38,7 +39,7 @@ class Item
/**
* @throws FileNotFoundException
* @throws UnreadableFileException
* @throws UnreadableFileEncountered
* @throws Exception
*/
public function __construct($file,int $action)
@ -54,7 +55,7 @@ class Item
throw new FileNotFoundException('Item doesnt exist: '.$file);
if (! is_readable($file))
throw new UnreadableFileException('Item cannot be read: '.$file);
throw new UnreadableFileEncountered('Item cannot be read: '.$file);
$this->file_name = $file;
$x = stat($file);
@ -130,6 +131,9 @@ class Item
if (strcasecmp(substr($x,1),'req') == 0)
return self::IS_REQ;
if (strcasecmp(substr($x,1),'tic') == 0)
return self::IS_TIC;
for ($i=0;$i<count($ext);$i++)
if (! strncasecmp($x,'.'.$ext[$i],strlen($ext[$i])) && (preg_match('/^[0-9a-z]/',strtolower(substr($x,3,1)))))
return self::IS_ARC;

View File

@ -10,7 +10,7 @@ use Symfony\Component\HttpFoundation\File\Exception\FileException;
use App\Classes\FTN\InvalidPacketException;
use App\Classes\FTN\Packet;
use App\Jobs\MessageProcess;
use App\Jobs\{MessageProcess,TicProcess};
use App\Models\Address;
/**
@ -170,6 +170,14 @@ final class Receive extends Item
break;
case self::IS_TIC:
Log::info(sprintf('%s: - Processing tic file [%s]',self::LOGKEY,$this->file));
// Queue the tic to be processed later, in case the referenced file hasnt been received yet
TicProcess::dispatch($this->file);
break;
default:
Log::debug(sprintf('%s: - Leaving file [%s] in the inbound dir',self::LOGKEY,$this->file));
}

View File

@ -6,7 +6,7 @@ use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use League\Flysystem\UnreadableFileException;
use League\Flysystem\UnreadableFileEncountered;
use App\Models\Address;
@ -135,7 +135,7 @@ final class Send extends Item
Log::error(sprintf('%s:! Item [%s] doesnt exist',self::LOGKEY,$file));
return;
} catch (UnreadableFileException) {
} catch (UnreadableFileEncountered) {
Log::error(sprintf('%s:! Item [%s] cannot be read',self::LOGKEY,$file));
return;
@ -257,7 +257,7 @@ final class Send extends Item
*
* @param int $length
* @return string|null
* @throws UnreadableFileException
* @throws UnreadableFileEncountered
* @throws Exception
*/
public function read(int $length): ?string
@ -276,7 +276,7 @@ final class Send extends Item
Log::debug(sprintf('%s: - Read [%d] bytes, file pos now [%d]',self::LOGKEY,strlen($data),$this->file_pos));
if ($data === FALSE)
throw new UnreadableFileException('Error reading file: '.$this->sending->file_name);
throw new UnreadableFileEncountered('Error reading file: '.$this->sending->file_name);
return $data;
}

View File

@ -7,7 +7,7 @@ use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use League\Flysystem\UnreadableFileException;
use League\Flysystem\UnreadableFileEncountered;
use App\Classes\Protocol as BaseProtocol;
use App\Classes\Sock\SocketClient;
@ -511,7 +511,7 @@ final class Binkp extends BaseProtocol
try {
$data = $this->send->read(self::BP_BLKSIZE);
} catch (UnreadableFileException) {
} catch (UnreadableFileEncountered) {
$this->send->close(FALSE);
$this->sessionClear(self::SE_SENDFILE);

View File

@ -3,7 +3,6 @@
namespace App\Classes\Protocol;
use Illuminate\Support\Facades\Log;
use League\Flysystem\UnreadableFileException;
use App\Classes\Protocol;
use App\Classes\Protocol\Zmodem as ZmodemClass;

View File

@ -0,0 +1,37 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Jobs\TicProcess as Job;
class TicProcess extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'tic:process {file : TIC file}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Process a TIC file';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
// Dispatch job.
Job::dispatchSync($this->argument('file'));
return Command::SUCCESS;
}
}

View File

@ -12,7 +12,7 @@ use Illuminate\Support\Facades\Notification;
use Illuminate\Support\ViewErrorBag;
use App\Http\Requests\SystemRegister;
use App\Models\{Address,Echoarea,Setup,System,SystemZone,Zone};
use App\Models\{Address,Echoarea,Filearea,Setup,System,SystemZone,Zone};
use App\Notifications\AddressLink;
use App\Rules\{FidoInteger,TwoByteInteger};
@ -410,6 +410,40 @@ class SystemController extends Controller
->with('echoareas',$eo);
}
/**
* Update the systems fileareas
*
* @param Request $request
* @param System $o
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse
*/
public function fileareas(Request $request,System $o)
{
$ao = $o->addresses->firstWhere('id',$request->address_id);
if (($request->method() == 'POST') && $request->post()) {
session()->flash('accordion','filearea');
// Ensure we have session details for this address.
if (! $ao->session('sespass'))
return redirect()->back()->withErrors('System doesnt belong to this network');
$ao->fileareas()->syncWithPivotValues($request->get('id',[]),['subscribed'=>Carbon::now()]);
return redirect()->back()->with('success','Fileareas updated');;
}
$fo = Filearea::active()
->where('domain_id',$ao->zone->domain_id)
->orderBy('name')
->get();
return view('system.widget.filearea')
->with('o',$o)
->with('ao',$ao)
->with('fileareas',$fo);
}
public function home()
{
return view('system.home');

40
app/Jobs/TicProcess.php Normal file
View File

@ -0,0 +1,40 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Classes\FTN\Tic;
class TicProcess implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private const LOGKEY = 'JTP';
/**
* Create a new job instance.
*
* @param string $file
*/
public function __construct(private string $file)
{
}
/**
* Execute the job.
*
* @return void
* @throws FileNotFoundException
*/
public function handle()
{
new Tic($this->file);
}
}

View File

@ -159,7 +159,29 @@ class Address extends Model
public function echomails()
{
return $this->belongsToMany(Echomail::class,'echomail_seenby')
->withPivot(['sent_at','packet']);
->withPivot(['sent_at','export_at','packet']);
}
/**
* Files that this address has seen
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function files()
{
return $this->belongsToMany(File::class,'file_seenby')
->withPivot(['sent_at','export_at']);
}
/**
* Echoareas this address is subscribed to
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function fileareas()
{
return $this->belongsToMany(Filearea::class)
->withPivot(['subscribed']);
}
/**
@ -409,7 +431,7 @@ class Address extends Model
}
/**
* Netmail waiting to be sent to this system
* Echomail waiting to be sent to this system
*
* @return Collection
*/
@ -421,6 +443,19 @@ class Address extends Model
->get();
}
/**
* Files waiting to be sent to this system
*
* @return Collection
*/
public function filesWaiting(): Collection
{
return $this->files()
->whereNull('file_seenby.sent_at')
->whereNotNull('file_seenby.export_at')
->get();
}
/**
* Get echomail for this node
*

157
app/Models/File.php Normal file
View File

@ -0,0 +1,157 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Rennokki\QueryCache\Traits\QueryCacheable;
use App\Casts\{CollectionOrNull,CompressedString};
use App\Traits\EncodeUTF8;
class File extends Model
{
use SoftDeletes,EncodeUTF8,QueryCacheable;
private const LOGKEY = 'MF-';
private bool $no_export = FALSE;
protected $casts = [
'kludges' => CollectionOrNull::class,
'rogue_seenby' => CollectionOrNull::class,
'rogue_path' => CollectionOrNull::class,
'desc' => CompressedString::class,
'ldesc' => CompressedString::class,
'size' => 'int',
];
private const cast_utf8 = [
'desc',
'ldesc',
];
protected $dates = ['datetime'];
public function __set($key,$value)
{
switch ($key) {
case 'fullname':
case 'replaces':
case 'no_export':
case 'set_path':
case 'set_packet':
case 'set_seenby':
$this->{$key} = $value;
break;
default:
parent::__set($key,$value);
}
}
public static function boot()
{
parent::boot();
static::creating(function($model) {
Log::debug(sprintf('%s:- Storing file [%s]',self::LOGKEY,$model->file));
// Store file
// Delete file from inbound
// Delete anything being replaced
});
// @todo if the file is updated with new SEEN-BY's from another route, we'll delete the pending export for systems (if there is one)
static::created(function($model) {
if (! $model->filearea_id) {
Log::alert(sprintf('%s:- File has no filearea, not exporting',self::LOGKEY,$model->id));
return;
}
$so = Setup::findOrFail(config('app.id'));
// Our address
$ftns = $so
->system
->match($model->fftn->zone,Address::NODE_ACTIVE|Address::NODE_PVT|Address::NODE_HOLD);
// Add our address to the seenby;
$model->set_seenby = $model->set_seenby->merge($ftns->pluck('id'))->unique();
$model->set_path = $model->set_path->merge([[
'address'=>$ftns->first(),
'datetime'=>($x=Carbon::now())->timestamp,
'extra'=>sprintf('%s %s (%s)',$x->toRfc7231String(),$so::PRODUCT_NAME,$so->version),
]]);
// Save the seenby
$model->seenby()->sync($model->set_seenby);
// Save the Path
$ppoid = NULL;
foreach ($model->set_path as $path) {
$po = DB::select('INSERT INTO file_path (file_id,address_id,parent_id,datetime,extra) VALUES (?,?,?,?,?) RETURNING id',[
$model->id,
$path['address']->id,
$ppoid,
Carbon::createFromTimestamp($path['datetime']),
$path['extra'],
]);
$ppoid = $po[0]->id;
}
// See if we need to export this message.
$exportto = $model->filearea->addresses->pluck('id')->diff($model->set_seenby);
if ($exportto->count()) {
if ($model->no_export) {
Log::debug(sprintf('%s:- NOT processing exporting of message by configuration [%s] to [%s]',self::LOGKEY,$model->id,$exportto->join(',')));
return;
}
Log::debug(sprintf('%s:- Exporting file [%s] to [%s]',self::LOGKEY,$model->id,$exportto->join(',')));
// Save the seenby for the exported systems
$model->seenby()->syncWithPivotValues($exportto,['export_at'=>Carbon::now()],FALSE);
}
});
}
/* RELATIONS */
public function filearea()
{
return $this->belongsTo(Filearea::class);
}
public function fftn()
{
return $this->belongsTo(Address::class)
->withTrashed();
}
public function seenby()
{
return $this->belongsToMany(Address::class,'file_seenby')
->ftnOrder();
}
public function path()
{
return $this->belongsToMany(Address::class,'file_path')
->withPivot(['id','parent_id','extra']);
}
/* METHODS */
public function jsonSerialize(): array
{
return $this->encode();
}
}

View File

@ -11,6 +11,10 @@ class Filearea extends Model
{
use SoftDeletes,ScopeActive;
protected $fillable = [
'name',
];
/* RELATIONS */
public function addresses()

View File

@ -92,6 +92,14 @@ class System extends Model
->where('addresses.system_id',$this->id);
}
public function fileareas()
{
return Filearea::select('fileareas.*')
->join('address_filearea',['address_filearea.filearea_id'=>'fileareas.id'])
->join('addresses',['addresses.id'=>'address_filearea.address_id'])
->where('addresses.system_id',$this->id);
}
/**
* Return the system name, or role name for the zone
*

View File

@ -0,0 +1,85 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('files', function(Blueprint $table) {
$table->id();
$table->timestamps();
$table->softDeletes();
$table->string('file');
$table->string('magic')->nullable();
$table->integer('size');
$table->bigInteger('crc');
$table->datetime('datetime');
$table->string('lfile')->nullable();
$table->string('desc')->nullable();
$table->text('ldesc')->nullable();
$table->jsonb('rogue_path')->nullable();
$table->jsonb('rogue_seenby')->nullable();
$table->jsonb('kludges')->nullable();
$table->bigInteger('filearea_id');
$table->foreign('filearea_id')->references('id')->on('fileareas');
$table->bigInteger('fftn_id');
$table->foreign('fftn_id')->references('id')->on('addresses');
});
Schema::create('file_seenby', function (Blueprint $table) {
$table->bigInteger('address_id');
$table->foreign('address_id')->references('id')->on('addresses');
$table->bigInteger('file_id');
$table->foreign('file_id')->references('id')->on('files');
$table->datetime('export_at')->nullable();
$table->datetime('sent_at')->nullable();
$table->unique(['address_id','file_id']);
});
Schema::create('file_path', function (Blueprint $table) {
$table->id();
$table->bigInteger('address_id');
$table->foreign('address_id')->references('id')->on('addresses');
$table->unique(['id','file_id']);
$table->unique(['file_id','address_id','parent_id']);
$table->bigInteger('parent_id')->nullable();
$table->foreign(['parent_id','file_id'])->references(['id','file_id'])->on('file_path');
$table->bigInteger('file_id');
$table->foreign('file_id')->references('id')->on('files');
$table->datetime('datetime');
$table->string('extra')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('file_path');
Schema::dropIfExists('file_seenby');
Schema::dropIfExists('files');
}
};

View File

@ -1,13 +1,10 @@
<!-- $o=System::class -->
@extends('layouts.app')
@section('htmlheader_title')
@can('admin',$o) @if($o->exists) Update @else Add @endif @endcan System
@endsection
@php
use App\Models\Address as Address;
@endphp
@section('content')
@if($o->exists)
<h1>{{ $o->name }}@if($o->setup)<sup class="success" style="text-shadow: 0 0; font-size: 50%; top: -1em;">*</sup>@endif</h1>
@ -213,6 +210,18 @@ use App\Models\Address as Address;
</div>
</div>
<!-- Fileareas -->
<div class="accordion-item">
<h3 class="accordion-header" id="filearea" data-bs-toggle="collapse" data-bs-target="#collapse_filearea" aria-expanded="false" aria-controls="collapse_filearea">File Area Subscription</h3>
<div id="collapse_filearea" class="accordion-collapse collapse {{ ($flash=='filearea') ? 'show' : '' }}" aria-labelledby="filearea" data-bs-parent="#accordion_homepage">
<div class="accordion-body">
<p>This system can subscribe to the following fileareas:</p>
@include('system.form-filearea')
</div>
</div>
</div>
<!-- Routing -->
<div class="accordion-item">
<h3 class="accordion-header" id="routing" data-bs-toggle="collapse" data-bs-target="#collapse_routing" aria-expanded="false" aria-controls="collapse_routing">Mail Routing</h3>
@ -289,15 +298,15 @@ use App\Models\Address as Address;
</div>
</div>
<!-- Waiting Mail -->
<!-- Items Waiting -->
<div class="accordion-item">
<h3 class="accordion-header" id="mail" data-bs-toggle="collapse" data-bs-target="#collapse_mail" aria-expanded="false" aria-controls="collapse_mail">Mail Waiting</h3>
<h3 class="accordion-header" id="mail" data-bs-toggle="collapse" data-bs-target="#collapse_mail" aria-expanded="false" aria-controls="collapse_mail">Items Waiting</h3>
<div id="collapse_mail" class="accordion-collapse collapse {{ ($flash=='mail') ? 'show' : '' }}" aria-labelledby="mail" data-bs-parent="#accordion_homepage">
<div class="accordion-body">
<div class="row">
<!-- Netmail -->
<div class="col-6">
<div class="col-4">
Netmails are waiting for these addresses:
<table class="table monotable">
<thead>
@ -319,7 +328,7 @@ use App\Models\Address as Address;
</div>
<!-- Echomail -->
<div class="col-6">
<div class="col-4">
Echomail waiting for these addresses:
<table class="table monotable">
<thead>
@ -339,6 +348,28 @@ use App\Models\Address as Address;
</tbody>
</table>
</div>
<!-- Files -->
<div class="col-4">
Files waiting for these addresses:
<table class="table monotable">
<thead>
<tr>
<th>Address</th>
<th>Files</th>
</tr>
</thead>
<tbody>
@foreach ($o->addresses->sortBy('zone.zone_id') as $ao)
<tr>
<td>{{ $ao->ftn3d }}</td>
<td>{{ $ao->filesWaiting()->count() }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>

View File

@ -13,10 +13,10 @@
<div class="row">
<!-- Select Domain -->
<div class="col-3">
<label for="domain_id" class="form-label">Network</label>
<label for="echoarea_domain_id" class="form-label">Network</label>
<div class="input-group has-validation">
<span class="input-group-text"><i class="bi bi-hash"></i></span>
<select class="form-select @error('domain_id') is-invalid @enderror" id="domain_id" name="domain_id" required>
<select class="form-select @error('domain_id') is-invalid @enderror" id="echoarea_domain_id" name="domain_id" required>
<option></option>
@foreach($x as $do)
<option value="{{ $do->id }}" @if(old('domain_id') == $do->id)selected @endif>{{ $do->id }} <small>({{ $do->name }})</small></option>
@ -32,11 +32,11 @@
<!-- Select Address -->
<div class="col-3">
<div class="d-none" id="address-select">
<label for="address_id" class="form-label">Address</label>
<div class="d-none" id="echoarea_address-select">
<label for="echoarea_address_id" class="form-label">Address</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-hash"></i></span>
<select class="form-select" id="address_id" name="address_id" required>
<select class="form-select" id="echoarea_address_id" name="address_id" required>
<option></option>
</select>
</div>
@ -79,13 +79,13 @@
<script type="text/javascript">
$(document).ready(function() {
$('#domain_id').on('change',function() {
$('#echoarea_domain_id').on('change',function() {
if (! $(this).val()) {
$('#echoarea-summary').removeClass('d-none');
$('#address-select').addClass('d-none');
$('#echoarea_address-select').addClass('d-none');
return;
} else {
$('#address-select').removeClass('d-none');
$('#echoarea_address-select').removeClass('d-none');
}
var item = this;
@ -99,7 +99,7 @@
},
success: function(data) {
icon.removeClass('spinner-grow spinner-grow-sm');
$('#address_id')
$('#echoarea_address_id')
.empty()
.append($('<option>'))
.append(data.map(function(item) {
@ -117,7 +117,7 @@
})
});
$('#address_id').on('change',function() {
$('#echoarea_address_id').on('change',function() {
if (! $(this).val()) {
$('#echoarea-summary').removeClass('d-none');
$('#echoarea-select').addClass('d-none');

View File

@ -0,0 +1,158 @@
@if(($x=\App\Models\Domain::active()
->select('domains.*')
->join('zones',['zones.domain_id'=>'domains.id'])
->whereIn('zone_id',$o->sessions->pluck('zone_id'))
->get())->count())
<form class="row g-0 needs-validation" method="post" action="{{ url('ftn/system/filearea',$o->id) }}" novalidate>
@csrf
<div class="row pt-0">
<div class="col-12">
<div class="greyframe titledbox shadow0xb0">
<div class="row">
<!-- Select Domain -->
<div class="col-3">
<label for="domain_id" class="form-label">Network</label>
<div class="input-group has-validation">
<span class="input-group-text"><i class="bi bi-hash"></i></span>
<select class="form-select @error('domain_id') is-invalid @enderror" id="filearea_domain_id" name="domain_id" required>
<option></option>
@foreach($x as $do)
<option value="{{ $do->id }}" @if(old('domain_id') == $do->id)selected @endif>{{ $do->id }} <small>({{ $do->name }})</small></option>
@endforeach
</select>
<span class="invalid-feedback" role="alert">
@error('domain_id')
{{ $message }}
@enderror
</span>
</div>
</div>
<!-- Select Address -->
<div class="col-3">
<div class="d-none" id="filearea_address-select">
<label for="filearea_address_id" class="form-label">Address</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-hash"></i></span>
<select class="form-select" id="filearea_address_id" name="address_id" required>
<option></option>
</select>
</div>
</div>
</div>
<!-- Summary of Addresses -->
<div class="offset-3 col-3" id="filearea-summary">
<table class="table monotable">
<thead>
<tr>
<th>Network</th>
<th class="text-end">Areas</th>
</tr>
</thead>
<tbody>
@foreach ($o->fileareas()->with(['domain'])->get()->groupBy('domain_id') as $oo)
<tr>
<td>{{ $oo->first()->domain->name }}</td>
<td class="text-end">{{ $oo->count() }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-12 d-none" id="filearea-select"></div>
</div>
</div>
</div>
</div>
</form>
@endif
@section('page-scripts')
<script type="text/javascript" src="{{ asset('plugin/checkboxes/jquery.checkboxes-1.2.2.min.js') }}"></script>
<script type="text/javascript">
$(document).ready(function() {
$('#filearea_domain_id').on('change',function() {
if (! $(this).val()) {
$('#filearea-summary').removeClass('d-none');
$('#filearea_address-select').addClass('d-none');
return;
} else {
$('#filearea_address-select').removeClass('d-none');
}
var item = this;
icon = $(item).parent().find('i');
$.ajax({
type: 'POST',
data: {domain_id: $(item).val()},
beforeSend: function() {
icon.addClass('spinner-grow spinner-grow-sm');
},
success: function(data) {
icon.removeClass('spinner-grow spinner-grow-sm');
$('#filearea_address_id')
.empty()
.append($('<option>'))
.append(data.map(function(item) {
return $('<option>').val(item.id).text(item.value);
}));
},
error: function(e) {
icon.removeClass('spinner-grow spinner-grow-sm');
if (e.status != 412)
alert('That didnt work? Please try again....');
},
url: '{{ url('api/system/address',[$o->id]) }}',
cache: false
})
});
$('#filearea_address_id').on('change',function() {
if (! $(this).val()) {
$('#filearea-summary').removeClass('d-none');
$('#filearea-select').addClass('d-none');
return;
}
if ($('#filearea-select').hasClass('d-none')) {
$('#filearea-select').removeClass('d-none');
$('#filearea-summary').addClass('d-none');
}
var item = this;
icon = $(item).parent().find('i');
$.ajax({
type: 'GET',
data: {address_id: $(item).val()},
beforeSend: function() {
icon.addClass('spinner-grow spinner-grow-sm');
},
success: function(data) {
icon.removeClass('spinner-grow spinner-grow-sm');
$('#filearea-select').empty().append(data);
$('#fileareas').checkboxes('range',true);
},
error: function(e) {
icon.removeClass('spinner-grow spinner-grow-sm');
if (e.status != 412)
alert('That didnt work? Please try again....');
},
url: '{{ url('ftn/system/filearea',[$o->id]) }}',
cache: false
})
});
});
</script>
@append

View File

@ -0,0 +1,44 @@
<div class="row">
<div class="col-12">
<table class="table monotable" id="fileareas">
<thead>
<tr>
<th>Subscribed</th>
<th>Filearea</th>
<th>Description</th>
</tr>
</thead>
<tbody>
@foreach ($fileareas as $oo)
<tr>
<td><input type="checkbox" name="id[]" value="{{ $oo->id }}" @if($ao->fileareas->search(function($item) use ($oo) { return $item->id == $oo->id; }) !== FALSE)checked @endif></td>
<td>{{ $oo->name }}</td>
<td>{{ $oo->description }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-2">
<a href="{{ url('ftn/system') }}" class="btn btn-danger">Cancel</a>
</div>
<span class="col-6 mt-auto mx-auto text-center align-bottom">
@if($errors->count())
<span class="btn btn-sm btn-danger" role="alert">
There were errors with the submission.
@dump($errors)
</span>
@endif
</span>
@can('admin',$o)
<div class="col-2">
<button type="submit" name="submit" class="btn btn-success float-end">Add</button>
</div>
@endcan
</div>

View File

@ -73,6 +73,8 @@ Route::middleware(['auth','verified','activeuser'])->group(function () {
->where('zo','[0-9]+');
Route::match(['get','post'],'ftn/system/echoarea/{o}',[SystemController::class,'echoareas'])
->where('o','[0-9]+');
Route::match(['get','post'],'ftn/system/filearea/{o}',[SystemController::class,'fileareas'])
->where('o','[0-9]+');
Route::match(['get','post'],'ftn/system/movaddress/{so}/{o}',[SystemController::class,'mov_address'])
->where('so','[0-9]+')
->where('o','[0-9]+');