authorize('update',$o); if ($request->post()) { foreach (['name','location','sysop','hold','phone','address','port','active','method','notes','zt_id','pkt_type','heartbeat'] as $key) $o->{$key} = $request->post($key); switch ($request->post('pollmode')) { case 1: $o->pollmode = FALSE; break; case 2: $o->pollmode = TRUE; break; default: $o->pollmode = NULL; } $o->autohold = FALSE; $o->save(); $mailers = collect($request->post('mailer_details')) ->filter(function($item) { return $item['port']; }) ->transform(function($item) { $item['active'] = Arr::get($item,'active',FALSE); return $item; }); $o->mailers()->sync($mailers); if ($request->post('users')) { if (array_filter($request->post('users'),function($item) { return $item; })) $o->users()->sync($request->post('users')); else $o->users()->detach(); } return redirect()->to('system'); } $o->load(['addresses.zone.domain','addresses.system','sessions.domain','sessions.systems']); return view('system.addedit') ->with('action',$o->exists ? 'update' : 'create') ->with('o',$o); } /** * Add an address to a system * * @param Request $request * @param System $o * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function address_add(Request $request,System $o) { // @todo a point address is failing validation // @todo This should be admin of the zone $this->authorize('admin',$o); session()->flash('accordion','address'); $request->validate([ 'action' => 'required|in:region,host,node,update', 'zone_id' => 'required|exists:zones,id', ]); switch ($request->post('action')) { case 'region': $request->validate([ 'region_id_new' => [ 'required', new TwoByteInteger, function ($attribute,$value,$fail) { // Check that the region doesnt already exist $o = Address::where(function($query) use ($value) { return $query->where('region_id',$value) ->where('host_id',0) ->where('node_id',0) ->where('point_id',0) ->where('role',Address::NODE_RC); }) // Check that a host doesnt already exist ->orWhere(function($query) use ($value) { return $query->where('host_id',$value) ->where('point_id',0) ->where('role',Address::NODE_NC); }); if ($o->count()) { $fail('Region or host already exists'); } }, ], ]); $oo = new Address; $oo->zone_id = $request->post('zone_id'); $oo->region_id = $request->post('region_id_new'); $oo->host_id = 0; $oo->node_id = 0; $oo->point_id = 0; $oo->role = Address::NODE_RC; $oo->active = TRUE; $o->addresses()->save($oo); break; case 'host': $request->validate([ 'region_id' => ['required',new FidoInteger], 'host_id_new' => [ 'required', new TwoByteInteger, function ($attribute,$value,$fail) use ($request) { // Check that the region doesnt already exist $o = Address::where(function($query) use ($value) { return $query->where(function($query) use ($value) { return $query->where('region_id',$value) ->where('role',Address::NODE_RC); }) // Check that a host doesnt already exist ->orWhere(function($query) use ($value) { return $query->where('host_id',$value) ->where('role',Address::NODE_NC); }); }) ->where('zone_id',$request->post('zone_id')) ->where('point_id',0) ->where('active',TRUE); if ($o->count()) { $fail('Region or host already exists'); } }, ], 'node_id_new' => [ 'required', new TwoByteInteger, function ($attribute,$value,$fail) use ($request) { // Check that the region doesnt already exist $o = Address::where(function($query) use ($request,$value) { return $query ->where('host_id',$request->post('host_id_new')) ->where('node_id',$value) ->where('point_id',0) ->where('role',Address::NODE_RC); }); if ($o->count()) { $fail('Host already exists'); } }, ] ]); // Find the Hub address // Find the zones /0 address, and assign it to this host. $oo = Address::where('zone_id',$request->zone_id) ->where('region_id',$request->region_id) ->where('host_id',$request->host_id_new) ->where('node_id',0) ->where('point_id',0) ->single(); // Its not defined, so we'll create it. if (! $oo) { $oo = new Address; $oo->forceFill([ 'zone_id'=>$request->zone_id, 'region_id'=>$request->region_id, 'host_id'=>$request->host_id_new, 'node_id'=>0, 'point_id'=>0, 'role'=>Address::NODE_NC, ]); } $oo->system_id = $request->system_id; $oo->active = TRUE; $o->addresses()->save($oo); $oo = new Address; $oo->zone_id = $request->post('zone_id'); $oo->region_id = $request->post('region_id'); $oo->host_id = $request->post('host_id_new'); $oo->node_id = $request->post('node_id_new'); $oo->point_id = 0; $oo->role = Address::NODE_ACTIVE; $oo->active = TRUE; $o->addresses()->save($oo); break; case 'update': case 'node': $request->validate([ 'region_id' => ['required',new FidoInteger], 'host_id' => ['required',new FidoInteger], 'node_id' => [ 'required', new TwoByteInteger, function ($attribute,$value,$fail) use ($request) { if ($request->point_id === 0) { // Check that the host doesnt already exist $o = Address::where(function($query) use ($request,$value) { return $query ->where('zone_id',$request->post('zone_id')) ->where('host_id',$request->post('host_id')) ->where('node_id',$value) ->where('point_id',0) ->where('id','<>',$request->post('submit')); }); if ($o->count()) { $fail(sprintf('Host already exists: %s',$o->get()->pluck('ftn')->join(','))); } } }, ], 'point_id' => [ 'required', function($attribute,$value,$fail) use ($request) { if (! is_numeric($value) || $value > Address::ADDRESS_FIELD_MAX) $fail(sprintf('Point numbers must be between 0 and %d',Address::ADDRESS_FIELD_MAX)); // Check that the host doesnt already exist $o = Address::where(function($query) use ($request,$value) { return $query ->where('zone_id',$request->post('zone_id')) ->where('host_id',$request->post('host_id')) ->where('node_id',$request->post('node_id')) ->where('point_id',$value) ->where('id','<>',$request->post('submit')); }); if ($o->count()) { $fail(sprintf('Point already exists: %s',$o->get()->pluck('ftn')->join(','))); } } ], 'hub' => 'required|boolean', 'hub_id' => 'nullable|exists:addresses,id', 'security' => 'required|integer|min:0|max:7', ]); $oo = Address::findOrNew($request->post('submit')); $oo->zone_id = $request->post('zone_id'); $oo->region_id = $request->post('region_id'); $oo->host_id = $request->post('host_id'); $oo->node_id = $request->post('node_id'); $oo->point_id = $request->post('point_id'); $oo->hub_id = $request->post('hub_id') > 0 ? $request->post('hub_id') : NULL; if (is_null($oo->role)) $oo->role = ((! $oo->point_id) && $request->post('hub')) ? Address::NODE_HC : ($request->post('point_id') ? Address::NODE_POINT : Address::NODE_ACTIVE); $oo->security = $request->post('security'); $oo->active = TRUE; $o->addresses()->save($oo); break; default: return redirect()->back()->withErrors(['action'=>'Unknown action: '.$request->post('action')]); } return redirect()->to(sprintf('system/addedit/%d',$o->id)); } /** * Delete address assigned to a host * * @param Address $o * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function address_del(Address $o) { // @todo This should be admin of the zone $this->authorize('admin',$o); session()->flash('accordion','address'); $sid = $o->system_id; $o->active = FALSE; $o->save(); $o->delete(); return redirect()->to(sprintf('system/addedit/%d',$sid)); } /** * Demote an address NC -> node * * @param Address $o * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function address_dem(Address $o) { // @todo This should be admin of the zone $this->authorize('admin',$o); session()->flash('accordion','address'); // Make sure that no other system has this address active. if ($o->role === Address::NODE_ACTIVE) return redirect()->back()->withErrors(['address'=>sprintf('%s cannot be demoted any more',$o->ftn3D)]); $o->role = ($o->role << 1); $o->save(); return redirect()->to(sprintf('system/addedit/%d',$o->system_id)); } public function address_merge(AddressMerge $request,int $id) { if ($request->validated()) { DB::beginTransaction(); // Find all the echomails that have both seenby's $x = Echomail::select(['id']) ->join('echomail_seenby',['echomail_seenby.echomail_id'=>'echomails.id']) ->whereIn('address_id',[$request->src]) ->distinct() ->with(['seenby:id']) ->get() ->filter(function($item) use ($request) { return $item->seenby->contains($request->dst) && $item->seenby->contains($request->src); }); // If the Echomail has both source and dest, delete the src if ($x->count()) DB::table('echomail_seenby') ->where('address_id',$request->src) ->whereIn('echomail_id',$x->pluck('id')) ->delete(); // Find all echomail seenbys $x = DB::update('update echomail_seenby set address_id=? where address_id=?',[$request->dst,$request->src]); // Find all echomail paths $x = DB::update('update echomail_path set address_id=? where address_id=?',[$request->dst,$request->src]); // Find all echomails $x = DB::update('update echomails set fftn_id=? where fftn_id=?',[$request->dst,$request->src]); // Find all netmails $x = DB::update('update netmails set fftn_id=? where fftn_id=?',[$request->dst,$request->src]); // Find all netmails $x = DB::update('update netmails set tftn_id=? where tftn_id=?',[$request->dst,$request->src]); // Find all nodelist $x = DB::update('update address_nodelist set address_id=? where address_id=?',[$request->dst,$request->src]); // Find all file seenbys $x = DB::update('update file_seenby set address_id=? where address_id=?',[$request->dst,$request->src]); // Find all files $x = DB::update('update files set fftn_id=? where fftn_id=?',[$request->dst,$request->src]); $src = Address::withTrashed()->findOrFail($request->src); // Resubscribe echoareas try { $x = DB::update('update address_echoarea set address_id=? where address_id=?',[$request->dst,$request->src]); } catch (QueryException $e) { DB::rollback(); return back()->withInput()->withErrors('error',sprintf('You may need to remove %s:%s (%d) from echoareas',$src->ftn,$src->system->name,$src->id)); } // Resubscribe fileareas try { $x = DB::update('update address_filearea set address_id=? where address_id=?',[$request->dst,$request->src]); } catch (QueryException $e) { DB::rollback(); return back()->withInput()->withErrors('error',sprintf('You may need to remove %s:%s (%d) from fileareas',$src->ftn,$src->system->name,$src->id)); } if ($src->forceDelete()) { DB::commit(); return redirect()->to('address/merge/'.$request->dst); } else { return back()->withInput()->withErrors('error',sprintf('Address [%s] didnt delete?',$src->ftn)); DB::rollBack(); } } $o = Address::withTrashed() ->findOrFail($id); $oo = Address::withTrashed() ->where('zone_id',$o->zone_id) ->where('host_id',$o->host_id) ->where('node_id',$o->node_id) ->where('point_id',$o->point_id) ->get(); if ($o->zone->domain->flatten) $oo = $oo->merge(Address::withTrashed() ->whereIn('zone_id',$o->zone->domain->zones->pluck('id')) ->where('host_id',$o->host_id) ->where('node_id',$o->node_id) ->where('point_id',$o->point_id) ->get() ); return view('system/address-merge') ->with('o',$o) ->with('oo',$oo); } /** * Move address to another system * * @param Request $request * @param System $so * @param Address $o * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function address_mov(Request $request,System $so,Address $o) { session()->flash('accordion','address'); // Quick check that this address belongs to this system if ($so->addresses->search(function($item) use ($o) { return $item->id === $o->id; }) === FALSE) abort(404); if ($request->post()) { $this->authorize('admin',$o); $validated = $request->validate([ 'system_id' => 'required|exists:systems,id', 'remove' => 'nullable|boolean', 'remsess' => 'nullable|boolean|exclude_if:remove,1', ]); $o->system_id = $validated['system_id']; $o->save(); if (Arr::get($validated,'remove')) { $so->sessions()->detach($o->zone); $so->mailers()->detach(); $so->users()->detach(); $so->delete(); } elseif (Arr::get($validated,'remsess')) { $so->sessions()->detach($o->zone); } return redirect()->to('system/addedit/'.$validated['system_id']); } return view('system.moveaddr') ->with('o',$o); } /** * Promote an address node -> NC * * @param Address $o * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function address_pro(Address $o) { // @todo This should be admin of the zone $this->authorize('admin',$o); session()->flash('accordion','address'); // Make sure that no other system has this address active. if ($o->role === Address::NODE_NC) return redirect()->back()->withErrors(['address'=>sprintf('%s cannot be promoted any more',$o->ftn3D)]); $o->role = ($o->role >> 1); $o->save(); return redirect()->to(sprintf('system/addedit/%d',$o->system_id)); } /** * Recover a deleted address * * @param int $id * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function address_pur(int $id) { $o = Address::onlyTrashed()->findOrFail($id); // @todo This should be admin of the zone $this->authorize('admin',$o); session()->flash('accordion','address'); $o->forceDelete(); return redirect()->to(sprintf('system/addedit/%d',$o->system_id)); } /** * Recover a deleted address * * @param int $id * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function address_rec(int $id) { $o = Address::onlyTrashed()->findOrFail($id); // @todo This should be admin of the zone $this->authorize('admin',$o); session()->flash('accordion','address'); $o->restore(); return redirect()->to(sprintf('system/addedit/%d',$o->system_id)); } /** * Suspend address assigned to a host * * @param Address $o * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function address_sus(Address $o) { // @todo This should be admin of the zone $this->authorize('admin',$o); session()->flash('accordion','address'); // Make sure that no other system has this address active. if (! $o->active && ($x=Address::where([ 'zone_id'=>$o->zone_id, 'host_id'=>$o->host_id, 'node_id'=>$o->node_id, 'point_id'=>$o->point_id, 'active'=>TRUE, ])->single())) { return redirect()->back()->withErrors(['address'=>sprintf('%s is already active on system [%s]',$o->ftn,url('system/addedit',$x->system_id),$x->system->name)]); } $o->active = (! $o->active); $o->save(); return redirect()->to(sprintf('system/addedit/%d',$o->system_id)); } public function api_address(Request $request,System $o): Collection { return Address::select(['addresses.id','addresses.zone_id','region_id','host_id','node_id','point_id']) ->leftjoin('zones',['zones.id'=>'addresses.zone_id']) ->where('addresses.system_id',$o->id) ->where('zones.domain_id',$request->domain_id) ->withTrashed() ->FTNorder() ->get() ->map(function($item) { return ['id'=>(string)$item->id,'value'=>$item->ftn4d]; }); } public function api_address_get(Address $o) { return $o; } /** * Identify all the addresses from systems that are not owned by a user * * @param Request $request * @return Collection */ public function api_address_orphan(Request $request): Collection { $result = collect(); list($zone_id,$host_id,$node_id,$point_id,$domain) = sscanf($request->query('term'),'%d:%d/%d.%d@%s'); # Look for Systems foreach (Address::select(['addresses.id','systems.name',DB::raw('systems.id AS system_id'),'zones.zone_id','region_id','host_id','node_id','point_id','addresses.zone_id']) ->join('zones',['zones.id'=>'addresses.zone_id']) ->rightjoin('systems',['systems.id'=>'addresses.system_id']) ->when($zone_id || $host_id || $node_id,function($query) use ($zone_id,$host_id,$node_id) { return $query ->when($zone_id,function($q,$zone_id) { return $q->where('zones.zone_id',$zone_id); }) ->where(function($q) use ($host_id) { return $q ->when($host_id,function($q,$host_id) { return $q->where('region_id',$host_id); }) ->when($host_id,function($q,$host_id) { return $q->orWhere('host_id',$host_id); }); }) ->when($node_id,function($q,$node_id) { return $q->where('node_id',$node_id); }); }) ->orWhere('systems.name','ilike','%'.$request->query('term').'%') ->orderBy('systems.name') ->get() as $o) { $result->push(['id'=>$o->id,'name'=>sprintf('%s (%s)',$o->ftn3d,$o->name),'category'=>'Systems']); } return $result; } public function api_address_validated_toggle(Request $request,string $state): array { $o = Address::findOrFail($request->id); $o->validated = $state === 'off' ? FALSE : TRUE; $o->save(); Log::debug(sprintf('%s:- Address Validated set to [%s]',self::LOGKEY,$o->autohold ? 'ON' : 'OFF')); return ['validated'=>$o->validated]; } public function api_autohold_toggle(Request $request,string $state): array { $o = System::findOrFail($request->id); $o->autohold = $state === 'off' ? FALSE : TRUE; $o->save(); Log::debug(sprintf('%s:- Autohold set to [%s]',self::LOGKEY,$o->autohold ? 'ON' : 'OFF')); return ['autohold'=>$o->autohold]; } public function areafix(AreafixRequest $request,System $o,Zone $zo) { if ($request->post()) { $no = new Netmail; foreach ($request->safe() as $item => $value) $no->{$item} = $value; $no->from = auth::user()->name; $no->msg .= "\r"; $no->datetime = Carbon::now(); $no->tzoffset = $no->datetime->utcOffset(); $no->flags = (Message::FLAG_LOCAL|Message::FLAG_PRIVATE|Message::FLAG_CRASH); $no->cost = 0; $no->tearline = sprintf('%s (%04X)',Setup::PRODUCT_NAME,Setup::PRODUCT_ID); $no->save(); Log::info(sprintf('%s:= Areafix to [%s], scheduling a poll',self::LOGKEY,$no->tftn->ftn)); AddressPoll::dispatch($no->tftn); return redirect()->back()->with('success','Areafix/Filefix sent'); } return view('system.areafix') ->with('zo',$zo) ->with('ao',$o->match($zo)->first()) ->with('o',$o) ->with('setup',Setup::findOrFail(config('app.id'))); } /** * Update the systems echoareas * * @param Request $request * @param System $o * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse */ public function echoareas(Request $request,System $o) { $ao = $o->addresses->firstWhere('id',$request->address_id); if (($request->method() === 'POST') && $request->post()) { session()->flash('accordion','echoarea'); if ($ao->trashed() && collect($request->get('id'))->diff($ao->echoareas->pluck('id'))->count()) return redirect()->back()->withErrors(sprintf('Address [%s] has been deleted, cannot add additional echos',$ao->ftn3d)); // Ensure we have session details for this address. if (! $ao->session('sespass')) return redirect()->back()->withErrors('System doesnt belong to this network'); $ao->echoareas()->syncWithPivotValues($request->get('id',[]),['subscribed'=>Carbon::now()]); return redirect()->back()->with('success','Echoareas updated'); } // @todo Allow a NC/RC/ZC to override $eo = Echoarea::active() ->where('domain_id',$ao->zone->domain_id) ->where(function($query) use ($ao) { return $query ->whereRaw(sprintf('(security&7) <= %d',$ao->security)) // write ->orWhereRaw(sprintf('((security>>3)&7) <= %d',$ao->security)); // read }) ->orderBy('name') ->get(); return view('system.widget.echoarea') ->with('o',$o) ->with('ao',$ao) ->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'); } // @todo Allow a NC/RC/ZC to override $fo = Filearea::active() ->where('domain_id',$ao->zone->domain_id) ->where(function($query) use ($ao) { return $query ->whereRaw(sprintf('(security&7) <= %d',$ao->security)) // write ->orWhereRaw(sprintf('((security>>3)&7) <= %d',$ao->security)); // read }) ->orderBy('name') ->get(); return view('system.widget.filearea') ->with('o',$o) ->with('ao',$ao) ->with('fileareas',$fo); } /** * Register a system, or link to an existing system */ public function register(SystemRegister $request) { // Step 1, show the user a form to select an existing defined system if ($request->isMethod('GET')) return view('user.system.register'); if ($request->action === 'register' && $request->name && is_numeric($request->name)) return view('user.system.widget.register_confirm') ->with('o',System::findOrFail($request->name)); $o = System::findOrNew(is_numeric($request->system_id) ? $request->system_id : NULL); // If the system exists, and we are 'register', we'll start the address claim process if ($o->exists && $request->action === 'Link') { $validate = Setup::findOrFail(config('app.id'))->system->inMyZones($o->addresses); // If we have addresses, we'll trigger the routed netmail if ($validate->count()) { Notification::route('netmail',$x=$validate->first())->notify(new AddressLink(Auth::user())); AddressPoll::dispatch($x)->delay(15); } return view('user.system.widget.register_send') ->with('validate',$validate) ->with('o',$o); } // If the system doesnt exist, we'll create it if (! $o->exist) { $o->sysop = Auth::user()->name; foreach (['name','zt_id','location','phone','method','address','port'] as $item) if ($request->{$item}) $o->{$item} = $request->{$item}; $o->active = TRUE; } if ($request->post('submit')) { Auth::user()->systems()->save($o); // @todo if the system already exists and part of one of our networks, we'll need to send the registration email to confirm the address. // @todo mark the system (or addresses) as "pending" at this stage until it is confirmed return redirect()->to(url('system/addedit',$o->id)); } // Re-flash our previously input data if ($request->old) session()->flashInput($request->old); return view('system.widget.system') ->with('action',$request->action) ->with('o',$o) ->with('errors',new ViewErrorBag); } /** * Add Session details * * @param Request $request * @param System $o * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function session_add(Request $request,System $o) { // @todo This should be admin of the zone $this->authorize('update',$o); session()->flash('accordion','session'); $validate = $request->validate([ 'zone_id' => 'required|exists:zones,id', 'sespass' => 'required|string|min:4', 'pktpass' => 'required|string|min:4|max:8', 'ticpass' => 'required|string|min:4', 'fixpass' => 'required|string|min:4', ]); $zo = Zone::findOrFail($validate['zone_id']); /* // @todo Disabling this, it needs improvement. If the new node is the ZC it becomes the default for the zone (and therefore remove all defaults from other addresses in the same zone), otherwise default should be false // If this session is for the ZC, it now becomes the default. if ($o->match($zo,Address::NODE_ZC)->count()) { SystemZone::where('default',TRUE)->update(['default'=>FALSE]); $validate['default'] = TRUE; } */ $o->sessions()->attach($zo,$validate); return redirect()->to(sprintf('system/addedit/%d',$o->id)); } /** * Delete address assigned to a host * * @param Address $o * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function session_del(System $o,Zone $zo) { $this->authorize('admin',$zo); session()->flash('accordion','session'); $o->sessions()->detach($zo); return redirect()->to(sprintf('system/addedit/%d',$o->id)); } // @todo Can this be consolidated with system_register() public function system_link(Request $request) { if (! $request->system_id) return redirect('user/system/register'); $so = System::findOrFail($request->system_id); $ca = NULL; $la = NULL; foreach (our_address() as $ao) { if (($ca=$so->match($ao->zone))->count()) break; } if ($ca->count() && $la=$ca->pop()) { Notification::route('netmail',$la)->notify(new AddressLink(Auth::user())); AddressPoll::dispatch($la)->delay(15); } return view('user.system.register_send') ->with('la',$la) ->with('o',$so); } public function view(System $o) { $o->load(['addresses']); return view('system.view') ->with('o',$o); } }