<?php namespace App\Http\Controllers; use Carbon\Carbon; use Illuminate\Support\Collection; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Notification; use Illuminate\Support\ViewErrorBag; use App\Http\Requests\SystemRegister; use App\Models\{Address,Echoarea,Filearea,Setup,System,SystemZone,Zone}; use App\Notifications\AddressLink; use App\Rules\{FidoInteger,TwoByteInteger}; class SystemController extends Controller { /** * Add an address to a system * * @param Request $request * @param System $o * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function add_address(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', '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 <HOST>/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 '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); }); 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 > DomainController::NUMBER_MAX) $fail(sprintf('Point numbers must be between 0 and %d',DomainController::NUMBER_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); }); if ($o->count()) { $fail(sprintf('Point already exists: %s',$o->get()->pluck('ftn')->join(','))); } } ], 'hub' => 'required|boolean', 'hub_id' => 'nullable|exists:addresses,id', ]); $oo = new Address; $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; $oo->role = ((! $oo->point_id) && $request->post('hub')) ? Address::NODE_HC : ($request->post('point_id') ? Address::NODE_POINT : Address::NODE_ACTIVE); $oo->active = TRUE; $o->addresses()->save($oo); break; default: return redirect()->back()->withErrors(['action'=>'Unknown action: '.$request->post('action')]); } return redirect()->to(sprintf('ftn/system/addedit/%d',$o->id)); } /** * Add Session details * * @param Request $request * @param System $o * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function add_session(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' => 'nullable|string|min:4|max:8', 'ticpass' => 'nullable|string|min:4', 'fixpass' => 'required|string|min:4', ]); $zo = Zone::findOrFail($validate['zone_id']); // 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('ftn/system/addedit/%d',$o->id)); } /** * Add or edit a node */ public function add_edit(SystemRegister $request,System $o) { $this->authorize('update',$o); if ($request->post()) { foreach (['name','location','sysop','hold','phone','address','port','active','method','notes','zt_id','pkt_type'] as $key) $o->{$key} = $request->post($key); $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); return redirect()->action([self::class,'home']); } $o->load(['addresses.zone.domain','addresses.system','sessions.domain','sessions.systems']); return view('system.addedit') ->with('action',$o->exists ? 'update' : 'create') ->with('o',$o); } public function api_address(Request $request,System $o): Collection { return Address::select(['addresses.id','addresses.zone_id','region_id','host_id','node_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->ftn3d]; }); } /** * Systems with no owners */ public function api_orphan(Request $request): Collection { return System::select(['id','name']) ->leftjoin('system_user',['system_user.system_id'=>'systems.id']) ->whereNull('user_id') ->where('systems.name','ilike','%'.$request->term.'%') ->orderBy('name') ->get(); } /** * Identify all the addresses from systems that are not owned by a user * * @param Request $request * @return Collection */ public function api_orphan_address(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; } /** * Delete address assigned to a host * * @param Address $o * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function del_address(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('ftn/system/addedit/%d',$sid)); } /** * Demo an address NC -> node * * @param Address $o * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function dem_address(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(['demaddress'=>sprintf('%s cannot be demoted any more',$o->ftn3D)]); $o->role = ($o->role << 1); $o->save(); return redirect()->to(sprintf('ftn/system/addedit/%d',$o->system_id)); } /** * Delete address assigned to a host * * @param Address $o * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function del_session(System $o,Zone $zo) { $this->authorize('admin',$zo); session()->flash('accordion','session'); $o->sessions()->detach($zo); return redirect()->to(sprintf('ftn/system/addedit/%d',$o->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'); } $eo = Echoarea::active() ->where('domain_id',$ao->zone->domain_id) ->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'); } $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'); } /** * 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 mov_address(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->delete(); } elseif (Arr::get($validated,'remsess')) { $so->sessions()->detach($o->zone); } return redirect()->to('ftn/system/addedit/'.$validated['system_id']); } return view('system.moveaddr') ->with('o',$o); } public function ours() { return view('system.ours'); } /** * Promote an address node -> NC * * @param Address $o * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function pro_address(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(['proaddress'=>sprintf('%s cannot be promoted any more',$o->ftn3D)]); $o->role = ($o->role >> 1); $o->save(); return redirect()->to(sprintf('ftn/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 sus_address(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(['susaddress'=>sprintf('%s is already active on system [<a href="%s">%s</a>]',$o->ftn,url('ftn/system/addedit',$x->system_id),$x->system->name)]); } $o->active = (! $o->active); $o->save(); return redirect()->to(sprintf('ftn/system/addedit/%d',$o->system_id)); } /** * Recover a deleted address * * @param int $id * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function rec_address(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('ftn/system/addedit/%d',$o->system_id)); } /** * Recover a deleted address * * @param int $id * @return \Illuminate\Http\RedirectResponse * @throws \Illuminate\Auth\Access\AuthorizationException */ public function pur_address(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('ftn/system/addedit/%d',$o->system_id)); } public function system_link(Request $request) { if (! $request->system_id) return redirect('user/system/register'); $s = Setup::findOrFail(config('app.id'))->system; $so = System::findOrFail($request->system_id); $ca = NULL; $la = NULL; foreach ($s->akas as $ao) { if (($ca=$so->match($ao->zone))->count()) break; } if ($ca->count() && $la=$ca->pop()) Notification::route('netmail',$la)->notify(new AddressLink($la,Auth::user())); return view('user.system.register_send') ->with('la',$la) ->with('o',$so); } /** * Register a system, or link to an existing system */ public function system_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->system_id) return view('user.system.widget.register_confirm') ->with('o',System::findOrFail($request->system_id)); $o = System::findOrNew($request->system_id); // 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($x,Auth::user())); 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('ftn/system/addedit',$o->id)); } // Re-flash our previously input data if ($request->old) session()->flashInput($request->old); return view('system.widget.form-system') ->with('action',$request->action) ->with('o',$o) ->with('errors',new ViewErrorBag); } public function view(System $o) { $o->load(['addresses']); return view('system.view') ->with('o',$o); } }