Fix address edit which reactivated the address, Fix correct icons when using address merge, We only advertise validated addresses and use validated addresses for routing, Show our address on known AKA screen
1430 lines
38 KiB
PHP
1430 lines
38 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Carbon\Carbon;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
use Illuminate\Database\QueryException;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
use App\Classes\FTN\{Message,Packet};
|
|
use App\Exceptions\InvalidFTNException;
|
|
use App\Traits\{QueryCacheableConfig,ScopeActive};
|
|
|
|
/**
|
|
* This represents an FTN AKA.
|
|
*
|
|
* If an address is active, it belongs to the system. There can only be 1 active FTN (z:h/n.p@d)
|
|
* If an address is not active, it belonged to the system at a point in time in the past.
|
|
*
|
|
* If an address is validated, we know that the system is using the address (we've confirmed that during a session).
|
|
* validated is update/removed is during a mailer session, confirming if the node has the address
|
|
* @todo Remove validated, if that system hasnt used the address for a defined period (eg: 30)
|
|
*
|
|
* We'll only trigger a poll to a system that we have mail for if active && validated, unless "forced".
|
|
*
|
|
* Any mail for that address will be delivered, if active && validated.
|
|
*
|
|
* Address status:
|
|
* + Active (active=true/validated=true) - mail can flow and in one of our networks (we have session details)
|
|
* + Pending (active=true/validated=false) - remote hasnt configured address during a session and in one of our networks
|
|
* + Known (active=true/validated=true) - the node presented this address, but we didnt auth it, and its a network we are not in
|
|
* + Unconfirm (active=true/validated=true) - the node presented this address, but we dont manage the address (it uses a different hub)
|
|
* + Nodelist (active=true/validated=true) - the node presented this address, but we dont manage the address and its in a recent nodelist
|
|
* + Delisted (active=false/validated=true) - this node shouldnt use this address any more
|
|
* + Freed (active=false/validated=false) - this node shouldnt is known to not be using this address any more
|
|
*
|
|
* Other Status
|
|
* + citizen - The address belongs to one of our jurisdiction (hub_id = our address, NC,RC,ZC)
|
|
* + foreign - The address doesnt belong to our jurisdiction
|
|
*
|
|
* @see \App\Http\Requests\AddressAdd::class for rules about AKA and role
|
|
*/
|
|
|
|
class Address extends Model
|
|
{
|
|
use ScopeActive,SoftDeletes,QueryCacheableConfig;
|
|
|
|
private const LOGKEY = 'MA-';
|
|
|
|
// http://ftsc.org/docs/frl-1028.002
|
|
public const ftn_regex = '(\d+):(\d+)/(\d+)(?:\.(\d+))?(?:@([a-zA-Z0-9\-_~]{0,8}))?';
|
|
|
|
public const NODE_ZC = 1<<0; // Zone
|
|
public const NODE_RC = 1<<1; // Region
|
|
public const NODE_NC = 1<<2; // Host
|
|
public const NODE_HC = 1<<3; // Hub
|
|
public const NODE_NN = 1<<4; // Node
|
|
public const NODE_PVT = 1<<5; // Pvt (we dont have address information) @todo
|
|
public const NODE_HOLD = 1<<6; // Hold (user has requested hold, we havent heard from the node for 7 days
|
|
public const NODE_DOWN = 1<<7; // Down we havent heard from the node for 30 days
|
|
public const NODE_POINT = 1<<8; // Point
|
|
public const NODE_KEEP = 1<<9; // Dont mark an address hold/down or de-list automatically
|
|
public const NODE_UNKNOWN = 1<<15; // Unknown
|
|
public const NODE_ALL = 0x811f; // Mask to catch all nodes, excluding their status
|
|
|
|
// http://ftsc.org/docs/frl-1002.001
|
|
public const ADDRESS_FIELD_MAX = 0x7fff; // Maximum value for a field in the address
|
|
|
|
protected $visible = ['zone_id','region_id','host_id','hub_id','node_id','point_id','security'];
|
|
|
|
/* STATIC */
|
|
|
|
public static function boot()
|
|
{
|
|
parent::boot();
|
|
|
|
// For indexes when deleting, we need to change active to FALSE
|
|
static::softDeleted(function($model) {
|
|
Log::debug(sprintf('%s:Deleting [%d], updating active to FALSE',self::LOGKEY,$model->id));
|
|
|
|
$model->active = FALSE;
|
|
$model->save();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create an FTN address associated with a system
|
|
*
|
|
* @param string $address
|
|
* @param System $so
|
|
* @return Address|null
|
|
* @throws \Exception
|
|
*/
|
|
public static function createFTN(string $address,System $so): ?self
|
|
{
|
|
$ftn = self::parseFTN($address);
|
|
|
|
if (! $ftn['d']) {
|
|
// See if we have a default domain for this domain
|
|
if ($ftn['z']) {
|
|
$zo = Zone::where('zone_id',$ftn['z'])->where('default',TRUE)->single();
|
|
|
|
if ($zo)
|
|
$ftn['d'] = $zo->domain->name;
|
|
|
|
else {
|
|
Log::alert(sprintf('%s:! Refusing to create address [%s] no domain available',self::LOGKEY,$address));
|
|
|
|
return NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
Log::debug(sprintf('%s:- Creating AKA [%s] for [%s]',self::LOGKEY,$address,$so->name));
|
|
|
|
// Check Domain exists
|
|
Domain::unguard();
|
|
$do = Domain::firstOrNew(['name'=>$ftn['d']]);
|
|
Domain::reguard();
|
|
|
|
if (! $do->exists) {
|
|
$do->public = TRUE;
|
|
$do->active = TRUE;
|
|
$do->notes = 'Auto created';
|
|
|
|
$do->save();
|
|
}
|
|
|
|
// If the zone is zero, see if this is a flatten domain, and if so, find an NC
|
|
if (($ftn['z'] === 0) && $do->flatten) {
|
|
$nc = self::findZone($do,$ftn['n'],0,0);
|
|
|
|
if ($nc) {
|
|
Log::info(sprintf('%s:- Setting zone to [%d]',self::LOGKEY,$nc->zone->zone_id));
|
|
$ftn['z'] = $nc->zone->zone_id;
|
|
}
|
|
}
|
|
|
|
// Create zone
|
|
Zone::unguard();
|
|
$zo = Zone::firstOrNew(['domain_id'=>$do->id,'zone_id'=>$ftn['z']]);
|
|
Zone::reguard();
|
|
|
|
if (! $zo->exists) {
|
|
$zo->active = TRUE;
|
|
$zo->notes = 'Auto created';
|
|
$zo->system_id = System::createUnknownSystem()->id;
|
|
|
|
$do->zones()->save($zo);
|
|
}
|
|
|
|
if (! $zo->active || ! $do->active) {
|
|
Log::alert(sprintf('%s:! Refusing to create address [%s] in disabled zone or domain',self::LOGKEY,$address));
|
|
|
|
return NULL;
|
|
}
|
|
|
|
// Create Address, assigned to $so
|
|
$o = new self;
|
|
$o->active = TRUE;
|
|
$o->zone_id = $zo->id;
|
|
$o->region_id = $ftn['r'];
|
|
$o->host_id = $ftn['n'];
|
|
$o->node_id = $ftn['f'];
|
|
$o->point_id = $ftn['p'];
|
|
|
|
try {
|
|
$so->addresses()->save($o);
|
|
|
|
} catch (QueryException $e) {
|
|
Log::error(sprintf('%s:! ERROR creating address [%s] (%s)',self::LOGKEY,$o->ftn,get_class($e)));
|
|
return NULL;
|
|
}
|
|
|
|
return $o;
|
|
}
|
|
|
|
/**
|
|
* Find a record in the DB for a node string, eg: 10:1/1.0
|
|
*
|
|
* @param string $address
|
|
* @param bool $trashed
|
|
* @param bool $recent
|
|
* @return Address|null
|
|
* @throws \Exception
|
|
*/
|
|
public static function findFTN(string $address,bool $trashed=FALSE,bool $recent=FALSE): ?self
|
|
{
|
|
$ftn = self::parseFTN($address);
|
|
$o = NULL;
|
|
|
|
$query = (new self)
|
|
->select('addresses.*')
|
|
->join('zones',['zones.id'=>'addresses.zone_id'])
|
|
->join('domains',['domains.id'=>'zones.domain_id'])
|
|
->when($trashed,function($query) use ($recent) {
|
|
return $query->withTrashed()
|
|
->orderBy('updated_at','DESC')
|
|
->when($recent,fn($query)=>$query->where(fn($query)=>$query
|
|
->where('deleted_at','>=',Carbon::now()->subMonth())->orWhereNull('deleted_at')));
|
|
},function($query) {
|
|
$query->active();
|
|
})
|
|
->where('zones.zone_id',$ftn['z'])
|
|
->where('node_id',$ftn['f'])
|
|
->where('point_id',$ftn['p'])
|
|
->when($ftn['d'],function($query,$domain) {
|
|
$query->where('domains.name',$domain);
|
|
},function($query) {
|
|
$query->where('zones.default',TRUE);
|
|
})
|
|
->orderBy('created_at','DESC');
|
|
|
|
$q = $query->clone();
|
|
|
|
// Are we looking for a region address
|
|
if (($ftn['f'] === 0) && ($ftn['p'] === 0))
|
|
$o = $query
|
|
->where('region_id',$ftn['n'])
|
|
->where('host_id',$ftn['n'])
|
|
->with(['system:id,active,name,address,pkt_msgs,last_session,hold'])
|
|
->first();
|
|
|
|
// Look for a normal address
|
|
if (! $o)
|
|
$o = $q
|
|
->where(function($q) use ($ftn) {
|
|
return $q
|
|
->where(function($q) use ($ftn) {
|
|
return $q
|
|
->where('region_id',$ftn['n'])
|
|
->where('host_id',$ftn['n']);
|
|
})
|
|
->orWhere('host_id',$ftn['n']);
|
|
})
|
|
->with(['system:id,active,name,address,pkt_msgs,last_session,hold'])
|
|
->first();
|
|
|
|
// Check and see if we are a flattened domain, our address might be available with a different zone.
|
|
// This occurs when we are parsing 2D addresses from SEEN-BY, but we have the zone
|
|
if (! $o && ($ftn['p'] === 0)) {
|
|
if ($ftn['d'])
|
|
$do = Domain::select('flatten')->where(['name'=>$ftn['d']])->single();
|
|
else {
|
|
$zo = Zone::where('zone_id',$ftn['z'])->where('default',TRUE)->with(['domain:id,flatten'])->single();
|
|
$do = $zo?->domain;
|
|
}
|
|
|
|
if ($do && $do->flatten && (($ftn['z'] === 0) || $do->zones->pluck('zone_id')->contains($ftn['z'])))
|
|
$o = self::findZone($do,$ftn['n'],$ftn['f'],$ftn['p'],$trashed);
|
|
}
|
|
|
|
return ($o && ($trashed || $o->system->active)) ? $o : NULL;
|
|
}
|
|
|
|
public static function newFTN(string $address): self
|
|
{
|
|
$ftn = self::parseFTN($address);
|
|
$do = $ftn['d'] ? Domain::where('name',$ftn['d'])->single() : NULL;
|
|
|
|
$o = new self;
|
|
$o->region_id = $ftn['r'];
|
|
$o->host_id = $ftn['n'];
|
|
$o->node_id = $ftn['f'];
|
|
$o->point_id = $ftn['p'];
|
|
|
|
$zo = Zone::where('zone_id',$ftn['z'])
|
|
->when($do,fn($query)=>$query->where('domain_id',$do->id))
|
|
->single();
|
|
|
|
$o->zone_id = $zo?->id;
|
|
|
|
if (($ftn['z'] === 0) || (! $zo)) {
|
|
Log::alert(sprintf('%s:! newFTN was parsed an FTN [%s] with a zero zone, adding empty zone in domain',self::LOGKEY,$address));
|
|
|
|
$zo = new Zone;
|
|
$zo->domain_id = $do?->id;
|
|
}
|
|
|
|
$o->zone()->associate($zo);
|
|
|
|
return $o;
|
|
}
|
|
|
|
/**
|
|
* This is to find an address for a domain (like fidonet), which is technically 2D even though it uses multiple zones.
|
|
*
|
|
* This was implemented to identify seenby and path kludges
|
|
*
|
|
* @param Domain $do
|
|
* @param int $host
|
|
* @param int $node
|
|
* @param int $point
|
|
* @param bool $trashed
|
|
* @return self|null
|
|
* @throws \Exception
|
|
*/
|
|
public static function findZone(Domain $do,int $host,int $node,int $point,bool $trashed=FALSE): ?self
|
|
{
|
|
if (! $do->flatten)
|
|
throw new \Exception(sprintf('Domain is not set with flatten: %d',$do->id));
|
|
|
|
$zones = $do->zones->pluck('zone_id');
|
|
|
|
$o = (new self)
|
|
->select('addresses.*')
|
|
->join('zones',['zones.id'=>'addresses.zone_id'])
|
|
->when($trashed,function($query) {
|
|
$query->withTrashed();
|
|
},function($query) {
|
|
$query->active();
|
|
})
|
|
->whereIN('zones.zone_id',$zones)
|
|
->where(function($q) use ($host) {
|
|
return $q
|
|
->where(function($q) use ($host) {
|
|
return $q->where('region_id',$host)
|
|
->where('host_id',$host);
|
|
})
|
|
->orWhere('host_id',$host);
|
|
})
|
|
->where('node_id',$node)
|
|
->where('point_id',$point)
|
|
->where('zones.domain_id',$do->id)
|
|
->single();
|
|
|
|
return $o;
|
|
}
|
|
|
|
/**
|
|
* Parse a string and split it out as an FTN array
|
|
*
|
|
* @param string $ftn
|
|
* @return array
|
|
* @throws \Exception
|
|
*/
|
|
public static function parseFTN(string $ftn): array
|
|
{
|
|
if (! preg_match(sprintf('#^%s$#',self::ftn_regex),strtolower($ftn),$matches))
|
|
throw new InvalidFTNException(sprintf('Invalid FTN: [%s] - regex failed',serialize($ftn)));
|
|
|
|
// Check our numbers are correct.
|
|
foreach ([1,2,3] as $i)
|
|
if ((! is_numeric($matches[$i])) || ($matches[$i] > self::ADDRESS_FIELD_MAX))
|
|
throw new InvalidFTNException(sprintf('Invalid FTN: [%s] - zone, host, or node address invalid [%d]',$ftn,$matches[$i]));
|
|
|
|
if ((! empty($matches[4])) AND ((! is_numeric($matches[$i])) || ($matches[4] > self::ADDRESS_FIELD_MAX)))
|
|
throw new InvalidFTNException(sprintf('Invalid FTN: [%s] - point address invalid [%d]',$ftn,$matches[4]));
|
|
|
|
// Work out region
|
|
$region_id = NULL;
|
|
$zone_id = NULL;
|
|
// We can only work out region/zone if we have a domain - this is for 2D parsing
|
|
if ($matches[5] ?? NULL) {
|
|
$o = new self;
|
|
$o->host_id = (int)$matches[2];
|
|
$o->node_id = (int)$matches[3];
|
|
$o->point_id = empty($matches[4]) ? 0 : (int)$matches[4];
|
|
|
|
if ($matches[1] !== "0") {
|
|
$zo = Zone::select('zones.*')
|
|
->where('zone_id',$matches[1])
|
|
->join('domains',['domains.id'=>'zones.domain_id'])
|
|
->where('domains.name',$matches[5])
|
|
->single();
|
|
|
|
// Try and find out the zone from the host_id
|
|
} else {
|
|
$zo = Zone::select('zones.*')
|
|
->where(fn($query)=>$query->where('host_id',$matches[2])->orWhere('region_id',$matches[2]))
|
|
->join('domains',['domains.id'=>'zones.domain_id'])
|
|
->join('addresses',['addresses.zone_id'=>'zones.id'])
|
|
->where('domains.name',$matches[5])
|
|
->first();
|
|
}
|
|
|
|
$o->zone_id = $zo?->id;
|
|
$parent = $o->parent();
|
|
|
|
$region_id = $parent?->region_id;
|
|
$zone_id = $parent?->zone->zone_id;
|
|
}
|
|
|
|
return [
|
|
'z' => (int)($zone_id ?: $matches[1]),
|
|
'r' => (int)$region_id,
|
|
'n' => (int)$matches[2],
|
|
'f' => (int)$matches[3],
|
|
'p' => empty($matches[4]) ? 0 : (int)$matches[4],
|
|
'd' => $matches[5] ?? NULL
|
|
];
|
|
}
|
|
|
|
/* SCOPES */
|
|
|
|
/**
|
|
* An FTN is active only if the address, zone, domain is also active
|
|
*
|
|
* @param $query
|
|
* @return mixed
|
|
* @note zones and domains needs to be joined in the base call, or use FTN()
|
|
*/
|
|
public function scopeActiveFTN($query)
|
|
{
|
|
return $query
|
|
->where('zones.active',TRUE)
|
|
->where('domains.active',TRUE)
|
|
->active();
|
|
}
|
|
|
|
/**
|
|
* Select to support returning FTN address
|
|
*
|
|
* @param $query
|
|
* @return void
|
|
*/
|
|
public function scopeFTN($query)
|
|
{
|
|
return $query
|
|
->select(['addresses.id','region_id','host_id','node_id','point_id','addresses.zone_id','addresses.active','role','security','addresses.system_id','addresses.active','validated','deleted_at'])
|
|
->join('zones',['zones.id'=>'addresses.zone_id'])
|
|
->join('domains',['domains.id'=>'zones.domain_id'])
|
|
->orderBy('domains.name')
|
|
->FTNorder()
|
|
->with([
|
|
'zone:zones.id,domain_id,zone_id,active',
|
|
'zone.domain:domains.id,name,active,public',
|
|
]);
|
|
}
|
|
|
|
public function scopeFTNOrder($query)
|
|
{
|
|
return $query
|
|
->orderBy('region_id')
|
|
->orderBy('host_id')
|
|
->orderBy('node_id')
|
|
->orderBy('point_id');
|
|
}
|
|
|
|
public function scopeFTN2DOrder($query)
|
|
{
|
|
return $query
|
|
->orderBy('host_id')
|
|
->orderBy('node_id')
|
|
->orderBy('point_id');
|
|
}
|
|
|
|
/**
|
|
* Return a list of addresses and the amount of uncollected echomail
|
|
*
|
|
* @param $query
|
|
* @return mixed
|
|
*/
|
|
public function scopeUncollectedEchomail($query)
|
|
{
|
|
return $query
|
|
->join('echomail_seenby',['echomail_seenby.address_id'=>'addresses.id'])
|
|
->join('echomails',['echomails.id'=>'echomail_seenby.echomail_id'])
|
|
->whereNotNull('export_at')
|
|
->whereNull('sent_at')
|
|
->whereNull('echomails.deleted_at')
|
|
->groupBy('addresses.id')
|
|
->dontCache();
|
|
}
|
|
|
|
public function scopeUncollectedEchomailTotal($query)
|
|
{
|
|
return $query
|
|
->select([
|
|
'addresses.id',
|
|
'zone_id',
|
|
'host_id',
|
|
'node_id',
|
|
'point_id',
|
|
'system_id',
|
|
DB::raw('count(addresses.id) as uncollected_echomail'),
|
|
DB::raw('0 as uncollected_netmail'),
|
|
DB::raw('0 as uncollected_files'),
|
|
])
|
|
->UncollectedEchomail();
|
|
}
|
|
|
|
/**
|
|
* Return a list of addresses and the amount of uncollected files
|
|
*
|
|
* @param $query
|
|
* @return mixed
|
|
*/
|
|
public function scopeUncollectedFiles($query)
|
|
{
|
|
return $query
|
|
->join('file_seenby',['file_seenby.address_id'=>'addresses.id'])
|
|
->join('files',['files.id'=>'file_seenby.file_id'])
|
|
->whereNotNull('export_at')
|
|
->whereNull('sent_at')
|
|
->whereNull('files.deleted_at')
|
|
->groupBy('addresses.id')
|
|
->dontCache();
|
|
}
|
|
|
|
public function scopeUncollectedFilesTotal($query)
|
|
{
|
|
return $query
|
|
->select([
|
|
'addresses.id',
|
|
'zone_id',
|
|
'host_id',
|
|
'node_id',
|
|
'point_id',
|
|
'system_id',
|
|
DB::raw('0 as uncollected_echomail'),
|
|
DB::raw('0 as uncollected_netmail'),
|
|
DB::raw('count(addresses.id) as uncollected_files')
|
|
])
|
|
->UncollectedFiles();
|
|
}
|
|
|
|
public function scopeUncollectedNetmail($query)
|
|
{
|
|
return $query
|
|
->join('netmails',['netmails.tftn_id'=>'addresses.id'])
|
|
->where(function($query) {
|
|
return $query->whereRaw(sprintf('(flags & %d) > 0',Message::FLAG_INTRANSIT))
|
|
->orWhereRaw(sprintf('(flags & %d) > 0',Message::FLAG_LOCAL));
|
|
})
|
|
->whereRaw(sprintf('(flags & %d) = 0',Message::FLAG_SENT))
|
|
->whereNull('sent_pkt')
|
|
->whereNull('sent_at')
|
|
->whereNull('netmails.deleted_at')
|
|
->groupBy('addresses.id')
|
|
->dontCache();
|
|
}
|
|
|
|
/**
|
|
* Return a list of addresses and the amount of uncollected netmail
|
|
*
|
|
* @param $query
|
|
* @return mixed
|
|
*/
|
|
public function scopeUncollectedNetmailTotal($query)
|
|
{
|
|
return $query
|
|
->select([
|
|
'addresses.id',
|
|
'zone_id',
|
|
'host_id',
|
|
'node_id',
|
|
'point_id',
|
|
'system_id',
|
|
DB::raw('0 as uncollected_echomail'),
|
|
DB::raw('count(addresses.id) as uncollected_netmail'),
|
|
DB::raw('0 as uncollected_files')
|
|
])
|
|
->UncollectedNetmail();
|
|
}
|
|
|
|
/* RELATIONS */
|
|
|
|
public function domain()
|
|
{
|
|
return $this->hasOneThrough(Domain::class,Zone::class,'id','id','zone_id','domain_id');
|
|
}
|
|
|
|
public function dynamics()
|
|
{
|
|
return $this->hasMany(Dynamic::class);
|
|
}
|
|
|
|
/**
|
|
* Echoareas this address is subscribed to
|
|
*
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
|
*/
|
|
public function echoareas()
|
|
{
|
|
return $this->belongsToMany(Echoarea::class)
|
|
->using(AddressEchoarea::class)
|
|
->orderBy('name')
|
|
->withPivot(['subscribed']);
|
|
}
|
|
|
|
public function echomail_from()
|
|
{
|
|
return $this->hasMany(Echomail::class,'fftn_id','id')
|
|
->orderBy('datetime','DESC')
|
|
->limit(10);
|
|
}
|
|
|
|
/**
|
|
* Echomails that this address has seen
|
|
*
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
|
* @todo Rework echomail_seenby to not have a specific seenby recorded for the fftn_id, but automatically include it when generating seenbys.
|
|
*/
|
|
public function echomail_seen()
|
|
{
|
|
return $this->belongsToMany(Echomail::class,'echomail_seenby')
|
|
->withPivot(['export_at','sent_at','sent_pkt']);
|
|
}
|
|
|
|
/**
|
|
* Files that this address has seen
|
|
*
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
|
*/
|
|
public function file_seen(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
|
{
|
|
return $this->belongsToMany(File::class,'file_seenby')
|
|
->withPivot(['sent_at','export_at']);
|
|
}
|
|
|
|
/**
|
|
* Fileareas this address is subscribed to
|
|
*
|
|
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
|
*/
|
|
public function fileareas(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
|
{
|
|
return $this->belongsToMany(Filearea::class)
|
|
->orderBy('name')
|
|
->withPivot(['subscribed']);
|
|
}
|
|
|
|
/**
|
|
* If we are a hub, if role === NODE_HC and child entries have us as their hub_id
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function nodes_hub(): HasMany
|
|
{
|
|
return $this->hasMany(Address::class,'hub_id','id')
|
|
->select(['id','addresses.zone_id','region_id','host_id','node_id','point_id','system_id'])
|
|
->active()
|
|
->FTNorder()
|
|
->with([
|
|
'zone:zones.id,domain_id,zone_id',
|
|
'zone.domain:domains.id,name',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Return the nodes that belong to this NC/RC/ZC
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function nodes_net(): HasMany
|
|
{
|
|
return HasMany::noConstraints(
|
|
fn()=>$this->newHasMany(
|
|
(new self)->newQuery()
|
|
->where('zone_id',$this->zone_id)
|
|
->where('host_id',$this->host_id)
|
|
->where('point_id',0)
|
|
->whereNot('id',$this->id)
|
|
->active()
|
|
->FTNorder()
|
|
->with(['zone:id,domain_id,zone_id']),
|
|
$this,
|
|
NULL,
|
|
($this->role_id === self::NODE_NC) ? 'id' : NULL)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* If we are a boss node, return our children.
|
|
*
|
|
* @return HasMany
|
|
*/
|
|
public function nodes_point(): HasMany
|
|
{
|
|
return HasMany::noConstraints(
|
|
fn()=>$this->newHasMany(
|
|
(new self)->newQuery()
|
|
->where('zone_id',$this->zone_id)
|
|
->where('host_id',$this->host_id)
|
|
->where('node_id',$this->node_id)
|
|
->where('point_id','>',0)
|
|
->whereNot('id',$this->id)
|
|
->active()
|
|
->FTNorder()
|
|
->with(['zone:id,domain_id,zone_id']),
|
|
$this,
|
|
NULL,
|
|
($this->role_id !== self::NODE_POINT) ? 'id' : NULL)
|
|
);
|
|
}
|
|
|
|
public function nodes_region(): HasMany
|
|
{
|
|
return HasMany::noConstraints(
|
|
fn()=>$this->newHasMany(
|
|
(new self)->newQuery()
|
|
->where('zone_id',$this->zone_id)
|
|
->where('region_id',$this->region_id)
|
|
->whereNot('id',$this->id)
|
|
->active()
|
|
->FTNorder()
|
|
->with(['zone:id,domain_id,zone_id']),
|
|
$this,
|
|
NULL,
|
|
($this->role_id === self::NODE_RC) ? 'id' : NULL)
|
|
);
|
|
}
|
|
|
|
public function nodes_zone(): HasMany
|
|
{
|
|
return HasMany::noConstraints(
|
|
fn()=>$this->newHasMany(
|
|
(new self)->newQuery()
|
|
->where('zone_id',$this->zone_id)
|
|
->whereNot('id',$this->id)
|
|
->active()
|
|
->FTNorder()
|
|
->with(['zone:id,domain_id,zone_id']),
|
|
$this,
|
|
NULL,
|
|
($this->role_id === self::NODE_ZC) ? 'id' : NULL)
|
|
);
|
|
}
|
|
|
|
public function system()
|
|
{
|
|
return $this->belongsTo(System::class);
|
|
}
|
|
|
|
public function uplink_hub()
|
|
{
|
|
return $this->belongsTo(Address::class,'hub_id','id');
|
|
}
|
|
|
|
public function zone()
|
|
{
|
|
return $this->belongsTo(Zone::class);
|
|
}
|
|
|
|
/* ATTRIBUTES */
|
|
|
|
/**
|
|
* Return if this address is active
|
|
*
|
|
* @param bool $value
|
|
* @return bool
|
|
*/
|
|
public function getActiveAttribute(bool $value): bool
|
|
{
|
|
return $value && $this->getActiveDomainAttribute();
|
|
}
|
|
|
|
public function getActiveDomainAttribute(): bool
|
|
{
|
|
return $this->zone->active && $this->zone->domain->active;
|
|
}
|
|
|
|
/**
|
|
* Render the node name in full 5D
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getFTNAttribute(): string
|
|
{
|
|
if (! $this->relationLoaded('zone'))
|
|
$this->load(['zone:id,domain_id,active,zone_id','zone.domain:domains.id,name,active']);
|
|
|
|
return sprintf('%s@%s',$this->getFTN4DAttribute(),$this->zone->domain->name);
|
|
}
|
|
|
|
public function getFTN2DAttribute(): string
|
|
{
|
|
return sprintf('%d/%d',$this->host_id ?: $this->region_id,$this->node_id);
|
|
}
|
|
|
|
public function getFTN3DAttribute(): string
|
|
{
|
|
if (! $this->relationLoaded('zone'))
|
|
$this->load(['zone:id,domain_id,zone_id']);
|
|
|
|
if (! $this->zone)
|
|
throw new InvalidFTNException(sprintf('Invalid Zone for FTN address [%d/%d.%d@%s]',$this->host_id ?: $this->region_id,$this->node_id,$this->point_id,$this->domain?->name));
|
|
|
|
return sprintf('%d:%s',$this->zone->zone_id,$this->getFTN2DAttribute());
|
|
}
|
|
|
|
public function getFTN4DAttribute(): string
|
|
{
|
|
if (! $this->relationLoaded('zone'))
|
|
$this->load(['zone:id,domain_id,zone_id']);
|
|
|
|
return sprintf('%s.%d',$this->getFTN3DAttribute(),$this->point_id);
|
|
}
|
|
|
|
public function getIsDefaultRouteAttribute(): bool
|
|
{
|
|
return (! is_null($x=$this->session('default'))) && $x;
|
|
}
|
|
|
|
public function getIsDownAttribute(): bool
|
|
{
|
|
return $this->role & self::NODE_DOWN;
|
|
}
|
|
|
|
public function getIsHostedAttribute(): bool
|
|
{
|
|
return strlen($this->getPassSessionAttribute() ?: '') > 0;
|
|
}
|
|
|
|
public function getIsHoldAttribute(): bool
|
|
{
|
|
return $this->role & self::NODE_HOLD;
|
|
}
|
|
|
|
public function getIsOwnedAttribute(): bool
|
|
{
|
|
return $this->system->is_owned;
|
|
}
|
|
|
|
public function getIsPrivateAttribute(): bool
|
|
{
|
|
return (! $this->system->address);
|
|
}
|
|
|
|
/**
|
|
* Determine this address' role (without status)
|
|
*
|
|
* @return int
|
|
* @see \App\Http\Requests\AddressAdd::class
|
|
*/
|
|
public function getRoleIdAttribute(): int
|
|
{
|
|
static $warn = FALSE;
|
|
|
|
$val = ($this->role & self::NODE_ALL);
|
|
$role = $this->ftn_role();
|
|
|
|
if ($this->isRoleOverride($role)) {
|
|
if (! $warn) {
|
|
$warn = TRUE;
|
|
Log::alert(sprintf('%s:! Address ROLE [%d] is not consistent with what is expected [%d] for [%s]',self::LOGKEY,$val,$role,$this->ftn));
|
|
}
|
|
|
|
return $val;
|
|
|
|
} else
|
|
return $role;
|
|
}
|
|
|
|
/**
|
|
* Return a name for the role (without status)
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getRoleNameAttribute(): string
|
|
{
|
|
if ($this->getIsDownAttribute())
|
|
return 'DOWN';
|
|
if ($this->getIsHoldAttribute())
|
|
return 'HOLD';
|
|
if ($this->getIsPrivateAttribute())
|
|
return 'PVT';
|
|
|
|
switch ($this->role_id) {
|
|
case self::NODE_ZC:
|
|
return 'ZC';
|
|
case self::NODE_RC:
|
|
return 'RC';
|
|
case self::NODE_NC:
|
|
return 'NC';
|
|
case self::NODE_HC:
|
|
return 'HUB';
|
|
case self::NODE_NN:
|
|
return 'NODE';
|
|
case self::NODE_POINT:
|
|
return 'POINT';
|
|
default:
|
|
return $this->role_id;
|
|
}
|
|
}
|
|
|
|
public function getPassFixAttribute(): ?string
|
|
{
|
|
return strtoupper($this->session('fixpass'));
|
|
}
|
|
|
|
public function getPassPacketAttribute(): ?string
|
|
{
|
|
return strtoupper($this->session('pktpass'));
|
|
}
|
|
|
|
public function getPassSessionAttribute(): ?string
|
|
{
|
|
return $this->session('sespass');
|
|
}
|
|
|
|
public function getPassTicAttribute(): ?string
|
|
{
|
|
return strtoupper($this->session('ticpass'));
|
|
}
|
|
|
|
/* METHODS */
|
|
|
|
/**
|
|
* Check the user's activation code for this address is correct
|
|
*
|
|
* @param User $uo
|
|
* @param string $code
|
|
* @return bool
|
|
*/
|
|
public function activation_check(User $uo,string $code): bool
|
|
{
|
|
try {
|
|
Log::info(sprintf('%s:Checking Activation code [%s] is valid for user [%d]',self::LOGKEY,$code,$uo->id));
|
|
|
|
return ($code === $this->activation_set($uo));
|
|
|
|
} catch (\Exception $e) {
|
|
Log::error(sprintf('%s:! Activation code [%s] invalid for user [%d]',self::LOGKEY,$code,$uo->id));
|
|
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create an activation code for this address
|
|
*
|
|
* @param User $uo
|
|
* @return string
|
|
*/
|
|
public function activation_set(User $uo): string
|
|
{
|
|
return sprintf('%x:%s',
|
|
$this->id,
|
|
substr(md5(sprintf('%d:%x',$uo->id,timew($this->updated_at))),0,10)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This is the children of this record, as per normal FTN routing ZC -> RC -> NC -> HUB -> Node -> Point
|
|
*
|
|
* This a ZC would return all records for the Zone,
|
|
* An RC would only return records in the region, etc
|
|
*
|
|
* @return Collection
|
|
* @see self::parent()
|
|
*/
|
|
public function children(): Collection
|
|
{
|
|
// If we are a point, our parent is the boss
|
|
switch ($this->role_id) {
|
|
case self::NODE_NN: // Normal Nodes
|
|
$o = self::active()
|
|
->where('zone_id',$this->zone_id)
|
|
->where('region_id',$this->region_id)
|
|
->where('host_id',$this->host_id)
|
|
->where('node_id',$this->node_id);
|
|
|
|
break;
|
|
|
|
case self::NODE_HC: // Hubs
|
|
$o = self::active()
|
|
->where('zone_id',$this->zone_id)
|
|
->where('region_id',$this->region_id)
|
|
->where('host_id',$this->host_id)
|
|
->where('hub_id',$this->id)
|
|
->where('id','<>',$this->id)
|
|
->FTNorder()
|
|
->get();
|
|
|
|
// Need to add in points of this hub's nodes
|
|
return $o->merge(
|
|
self::active()
|
|
->where('zone_id',$this->zone_id)
|
|
->where('region_id',$this->region_id)
|
|
->where('host_id',$this->host_id)
|
|
->whereIn('node_id',$o->pluck('node_id'))
|
|
->where('point_id','<>',0)
|
|
->FTNorder()
|
|
->get()
|
|
);
|
|
|
|
case self::NODE_NC: // Nets
|
|
$o = self::active()
|
|
->where('zone_id',$this->zone_id)
|
|
->where('region_id',$this->region_id)
|
|
->where('host_id',$this->host_id);
|
|
|
|
break;
|
|
|
|
case self::NODE_RC: // Regions
|
|
$o = self::active()
|
|
->where('zone_id',$this->zone_id)
|
|
->where('region_id',$this->region_id);
|
|
|
|
break;
|
|
|
|
case self::NODE_ZC: // Zone
|
|
$o = self::active()
|
|
->where('zone_id',$this->zone_id);
|
|
|
|
break;
|
|
|
|
default:
|
|
return new Collection;
|
|
}
|
|
|
|
return $o
|
|
->where('id','<>',$this->id)
|
|
->FTNorder()
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* Contrast to children(), this takes into account authentication details, and we route mail to this
|
|
* address (because we have session details) and it's children.
|
|
*
|
|
* @return Collection
|
|
* @throws \Exception
|
|
* @see self::children()
|
|
* @see self::uplink()
|
|
*/
|
|
public function downlinks(): Collection
|
|
{
|
|
// We have no session data for this address, (and its not our address), by definition it has no children
|
|
if (! $this->is_hosted && (! our_address()->pluck('id')->contains($this->id)))
|
|
return new Collection;
|
|
|
|
// If this system is not marked to default route for this address
|
|
if (! $this->is_default_route) {
|
|
$children = $this->children()
|
|
->push($this);
|
|
|
|
// We route everything for this domain
|
|
} else {
|
|
$children = self::select('addresses.*')
|
|
->join('zones',['zones.id'=>'addresses.zone_id'])
|
|
->where('addresses.id','<>',$this->id)
|
|
->where('domain_id',$this->zone->domain_id)
|
|
->with(['zone.domain'])
|
|
->active()
|
|
->FTNorder()
|
|
->get();
|
|
}
|
|
|
|
// If there are no children
|
|
if (! $children->count())
|
|
return new Collection;
|
|
|
|
// Exclude links and their children.
|
|
$exclude = collect();
|
|
foreach (our_nodes($this->zone->domain)->diff([$this]) as $o) {
|
|
// We only exclude downlink children
|
|
if ($o->role_id < $this->role_id)
|
|
continue;
|
|
|
|
// If this address is in our list, remove it and it's children
|
|
$exclude = $exclude->merge($o->children());
|
|
$exclude->push($o);
|
|
}
|
|
|
|
return $children->diff($exclude);
|
|
}
|
|
|
|
/**
|
|
* Files waiting to be sent to this system
|
|
*
|
|
* @return Collection
|
|
*/
|
|
public function dynamicWaiting(): Collection
|
|
{
|
|
return $this->dynamics()
|
|
->where('next_at','<=',Carbon::now())
|
|
->where('active',TRUE)
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* Echomail waiting to be sent to this system
|
|
*
|
|
* @return Builder
|
|
*/
|
|
public function echomailWaiting(): Builder
|
|
{
|
|
return Echomail::select('echomails.*')
|
|
->join('echomail_seenby',['echomail_seenby.echomail_id'=>'echomails.id'])
|
|
->where('address_id',$this->id)
|
|
->whereNull('echomails.deleted_at')
|
|
->whereNotNull('export_at')
|
|
->whereNull('sent_at')
|
|
->orderby('id')
|
|
->with([
|
|
'tagline:id,value',
|
|
'tearline:id,value',
|
|
'origin:id,value',
|
|
'echoarea:id,name,domain_id',
|
|
'echoarea.domain:id,name',
|
|
'fftn:id,zone_id,region_id,host_id,node_id,point_id',
|
|
'fftn.zone:id,domain_id,zone_id',
|
|
'fftn.zone.domain:id,name',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Files waiting to be sent to this system
|
|
*
|
|
* @return Collection
|
|
*/
|
|
public function filesWaiting(): Collection
|
|
{
|
|
return File::select('files.*')
|
|
->join('file_seenby',['file_seenby.file_id'=>'files.id'])
|
|
->where('address_id',$this->id)
|
|
->whereNull('files.deleted_at')
|
|
->whereNotNull('export_at')
|
|
->whereNull('sent_at')
|
|
->orderby('id')
|
|
->with([
|
|
'filearea:id,name,domain_id',
|
|
'filearea.domain:id,name',
|
|
'fftn:id,zone_id,region_id,host_id,node_id,point_id',
|
|
'fftn.zone:id,domain_id,zone_id',
|
|
'fftn.zone.domain:id,name',
|
|
])
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* Work out what role this FTN should have
|
|
*
|
|
* @return int|null
|
|
*/
|
|
private function ftn_role(): ?int
|
|
{
|
|
$role = NULL;
|
|
|
|
// If we have a point address, we're a point
|
|
if ($this->point_id)
|
|
$role = self::NODE_POINT;
|
|
|
|
// If we have a node_id, we're either a Node or a Hub
|
|
elseif ($this->node_id) {
|
|
$role = ($this->nodes_hub->count())
|
|
? self::NODE_HC
|
|
: ((($this->role & Address::NODE_ALL) === self::NODE_HC) ? self::NODE_HC : self::NODE_NN);
|
|
|
|
// point_id and node_id are zero
|
|
// If our region_id !== host_id, and are not zero, and node_id/point_id === 0, we are an NC
|
|
} elseif (($this->region_id !== $this->host_id) && $this->host_id) {
|
|
$role = self::NODE_NC;
|
|
|
|
// point_id and node_id are zero
|
|
} elseif (($this->region_id === $this->host_id) && $this->host_id) {
|
|
$role = self::NODE_RC;
|
|
|
|
// point_id and node_id are zero
|
|
} elseif (($this->region_id === $this->host_id) && (! $this->host_id)) {
|
|
$role = self::NODE_ZC;
|
|
}
|
|
|
|
if (isset($this->region_id) && is_null($role))
|
|
Log::alert(sprintf('%s:! Address ROLE [%d] could not be determined for [%s]',self::LOGKEY,($this->role & Address::NODE_ALL),$this->ftn));
|
|
|
|
return $role;
|
|
}
|
|
|
|
/**
|
|
* Get echomail for this node
|
|
*
|
|
* @return Packet|null
|
|
* @throws \Exception
|
|
* @todo If we export to uplink hubs without our address in the seenby, they should send the message back to
|
|
* us with their seenby's.
|
|
*/
|
|
public function getEchomail(): ?Packet
|
|
{
|
|
if ($count=($num=$this->echomailWaiting())->count()) {
|
|
Log::info(sprintf('%s:= Got [%d] echomails for [%s] for sending',self::LOGKEY,$count,$this->ftn));
|
|
|
|
// Limit to max messages
|
|
if ($count > $this->system->pkt_msgs)
|
|
Log::notice(sprintf('%s:= Only sending [%d] echomails for [%s]',self::LOGKEY,$this->system->pkt_msgs,$this->ftn));
|
|
|
|
return $this->system->packet($this)->mail($num->take($this->system->pkt_msgs)->get());
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
public function getFiles(): Collection
|
|
{
|
|
if ($count=($num=$this->filesWaiting())->count()) {
|
|
Log::info(sprintf('%s:= Got [%d] files for [%s] for sending',self::LOGKEY,$count,$this->ftn));
|
|
|
|
// Limit to max messages
|
|
if ($count > $this->system->batch_files)
|
|
Log::notice(sprintf('%s:= Only sending [%d] files for [%s]',self::LOGKEY,$this->system->batch_files,$this->ftn));
|
|
|
|
return $num->take($this->system->batch_files);
|
|
}
|
|
|
|
return new Collection;
|
|
}
|
|
|
|
/**
|
|
* Get netmail for this node (including it's children)
|
|
*
|
|
* @return Packet|null
|
|
* @throws \Exception
|
|
*/
|
|
public function getNetmail(): ?Packet
|
|
{
|
|
if ($count=($num=$this->netmailAlertWaiting())->count()) {
|
|
Log::info(sprintf('%s:= Packaging [%d] netmail alerts to [%s]',self::LOGKEY,$count,$this->ftn));
|
|
|
|
$msgs = $num->get();
|
|
|
|
// Find any message that defines a packet password
|
|
$pass = $msgs
|
|
->map(function($item) {
|
|
$passpos = strpos($item->subject,':');
|
|
return (($passpos > 0) && ($passpos <= 8)) ? substr($item->subject,0,$passpos) : NULL;
|
|
})
|
|
->filter()
|
|
->pop();
|
|
|
|
Log::debug(sprintf('%s:= Overwriting system packet password with [%s] for [%s]',self::LOGKEY,$pass,$this->ftn));
|
|
|
|
return $this->system->packet($this,$pass)->mail(
|
|
$msgs->filter(fn($item)=>preg_match("/^{$pass}:/",$item->subject))
|
|
->transform(function($item) use ($pass) {
|
|
$item->subject = preg_replace("/^{$pass}:/",'',$item->subject);
|
|
return $item;
|
|
})
|
|
);
|
|
}
|
|
|
|
if ($count=($num=$this->netmailWaiting())->count()) {
|
|
Log::info(sprintf('%s:= Got [%d] netmails for [%s] for sending',self::LOGKEY,$count,$this->ftn));
|
|
|
|
// Limit to max messages
|
|
if ($count > $this->system->pkt_msgs)
|
|
Log::alert(sprintf('%s:= Only sending [%d] netmails for [%s]',self::LOGKEY,$this->system->pkt_msgs,$this->ftn));
|
|
|
|
return $this->system->packet($this)->mail($num->take($this->system->pkt_msgs)->get());
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
public function isRoleOverride(int $role=NULL): bool
|
|
{
|
|
$val = ($this->role & self::NODE_ALL);
|
|
$role = $role ?: $this->ftn_role();
|
|
|
|
return ($val && ($role !== $val)) || (! $role);
|
|
}
|
|
|
|
/**
|
|
* Netmail waiting to be sent to this system
|
|
*
|
|
* @return Builder
|
|
* @throws \Exception
|
|
*/
|
|
public function netmailWaiting(): Builder
|
|
{
|
|
// Addresses that our downstream of this address, except anybody that has session details with us
|
|
$ours = our_nodes($this->zone->domain)->pluck('id');
|
|
|
|
$addresses = $this->downlinks()
|
|
->filter(fn($item)=>(! $ours->contains($item->id)))
|
|
->merge($this->system->match($this->zone,Address::NODE_ALL));
|
|
|
|
$netmails = $this
|
|
->UncollectedNetmail()
|
|
->select('netmails.id')
|
|
->whereIn('addresses.id',$addresses->pluck('id'))
|
|
->groupBy(['netmails.id'])
|
|
->get();
|
|
|
|
return Netmail::whereIn('id',$netmails->pluck('id'));
|
|
}
|
|
|
|
/**
|
|
* Netmail alerts waiting to be sent to this system
|
|
*
|
|
* @return Builder
|
|
* @throws \Exception
|
|
* @note The packet password to use is on the subject line for these alerts
|
|
*/
|
|
public function netmailAlertWaiting(): Builder
|
|
{
|
|
$netmails = $this
|
|
->UncollectedNetmail()
|
|
->whereRaw(sprintf('(flags & %d) > 0',Message::FLAG_LOCAL))
|
|
->whereRaw(sprintf('(flags & %d) > 0',Message::FLAG_PKTPASSWD))
|
|
->whereRaw(sprintf('(flags & %d) = 0',Message::FLAG_SENT))
|
|
->select('netmails.id')
|
|
->whereIn('addresses.id',$this->downlinks()->add($this)->pluck('id'))
|
|
->groupBy(['netmails.id'])
|
|
->get();
|
|
|
|
return Netmail::whereIn('id',$netmails->pluck('id'));
|
|
}
|
|
|
|
public function nodes(): Collection
|
|
{
|
|
switch ($this->role_id) {
|
|
case self::NODE_NN:
|
|
return $this->nodes_point;
|
|
|
|
case self::NODE_HC:
|
|
return $this->nodes_hub;
|
|
|
|
case self::NODE_NC:
|
|
return $this->nodes_net;
|
|
|
|
case self::NODE_RC:
|
|
return $this->nodes_region;
|
|
|
|
case self::NODE_ZC:
|
|
return $this->nodes_zone;
|
|
|
|
default:
|
|
return new Collection;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find the immediate parent for this address, as per normal FTN routing ZC <- RC <- NC <- HUB <- Node <- Point
|
|
*
|
|
* @return Address|null
|
|
* @throws \Exception
|
|
* @see self::children()
|
|
*/
|
|
public function parent(): ?Address
|
|
{
|
|
// If we are a point, our parent is the boss
|
|
switch ($this->role_id) {
|
|
case self::NODE_POINT: // BOSS Node
|
|
return self::active()
|
|
->where('zone_id',$this->zone_id)
|
|
->where('region_id',$this->region_id)
|
|
->where('host_id',$this->host_id)
|
|
->where('node_id',$this->node_id)
|
|
->where('point_id',0)
|
|
->single();
|
|
|
|
case self::NODE_NN: // HUB if it exists, otherwise NC
|
|
if ($this->uplink_hub)
|
|
return $this->uplink_hub;
|
|
|
|
// Else fall through
|
|
|
|
case self::NODE_HC: // NC
|
|
return self::active()
|
|
->where('zone_id',$this->zone_id)
|
|
->when($this->region_id,fn($q)=>$q->where('region_id',$this->region_id))
|
|
->where('host_id',$this->host_id)
|
|
->where('node_id',0)
|
|
->where('point_id',0)
|
|
->single();
|
|
|
|
case self::NODE_NC: // RC
|
|
return self::active()
|
|
->where('zone_id',$this->zone_id)
|
|
->where('region_id',$this->region_id)
|
|
->where('host_id',$this->region_id)
|
|
->where('node_id',0)
|
|
->where('point_id',0)
|
|
->single();
|
|
|
|
case self::NODE_RC: // ZC
|
|
return self::active()
|
|
->where('zone_id',$this->zone_id)
|
|
->where('region_id',0)
|
|
->where('node_id',0)
|
|
->where('point_id',0)
|
|
->single();
|
|
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve the address session details/passwords
|
|
*
|
|
* @param string $type
|
|
* @return string|null
|
|
*/
|
|
private function session(string $type): ?string
|
|
{
|
|
return ($x=$this->system?->sessions->where('id',$this->zone_id)->first()) ? ($x->pivot->{$type} ?: '') : NULL;
|
|
}
|
|
|
|
/**
|
|
* Contrast to parent(), this takes into account authentication details, and we route mail to this
|
|
* address (because we have session details) with that uplink.
|
|
*
|
|
* @return Address|$this|null
|
|
* @throws \Exception
|
|
* @see self::parent()
|
|
* @see self::downlinks()
|
|
*/
|
|
public function uplink(): ?Address
|
|
{
|
|
// If it is our address
|
|
if (our_address()->contains($this))
|
|
return NULL;
|
|
|
|
// If we have session password, then we are the parent
|
|
if ($this->is_hosted)
|
|
return $this;
|
|
|
|
// Traverse up our parents until we have one with session details
|
|
if ($x=$this->parent()?->uplink()) {
|
|
return $x;
|
|
|
|
// See if we have a node registered as the default route for this zone
|
|
} else {
|
|
$sz = SystemZone::whereIn('zone_id',$this->domain->zones->pluck('id'))
|
|
->where('default',TRUE)
|
|
->single();
|
|
|
|
return $sz?->system->akas->sortBy('security')->last();
|
|
}
|
|
}
|
|
} |