diff --git a/app/Classes/FTN/Message.php b/app/Classes/FTN/Message.php index 0c3228d..38fad90 100644 --- a/app/Classes/FTN/Message.php +++ b/app/Classes/FTN/Message.php @@ -242,25 +242,28 @@ class Message extends FTNBase $o->mo->msg_crc = md5($o->mo->msg_src); - $o->mo->fftn_id = $o->fftn?->id; + if ($o->fftn) + $o->mo->fftn_id = $o->fftn->id; + else + $o->mo->set_fftn = $o->fftn_t; switch (get_class($o->mo)) { case Echomail::class: + // Echomails dont have a to address break; case Netmail::class: - $o->mo->tftn_id = $o->tftn?->id; + if ($o->tftn) + $o->mo->tftn_id = $o->tftn->id; + else + $o->mo->set_tftn = $o->tftn_t; break; default: throw new InvalidPacketException('Unknown message class: '.get_class($o->mo)); } - if (($x=$o->validate())->fails()) { - $o->mo->errors = $x; - - Log::debug(sprintf('%s:! Message fails validation (%s@%s->%s@%s)',self::LOGKEY,$o->mo->from,$o->fftn_t,$o->mo->to,$o->tftn_t),['result'=>$o->mo->errors->errors()]); - } + $o->validate(); return $o->mo; } @@ -300,7 +303,7 @@ class Message extends FTNBase public function __get($key) { // @todo Do we need all these key values? - Log::debug(sprintf('%s:/ Requesting key for Message::class [%s]',self::LOGKEY,$key)); + //Log::debug(sprintf('%s:/ Requesting key for Message::class [%s]',self::LOGKEY,$key)); switch ($key) { // From Addresses @@ -556,7 +559,7 @@ class Message extends FTNBase foreach ($this->mo->kludges as $k=>$v) $return .= sprintf("\01%s %s\r",$k,$v); - $return .= $this->mo->content; + $return .= $this->mo->content."\r"; if ($this->mo instanceof Netmail) { foreach ($this->mo->path as $ao) @@ -830,14 +833,16 @@ class Message extends FTNBase $validator->errors()->add('invalid-zone',sprintf('Message zone [%d] doesnt match packet zone [%d].',$this->fz,$this->zone->zone_id)); } - if (! $this->fboss_o) - $validator->errors()->add('from',sprintf('Undefined Node [%s] sent message.',$this->fboss)); - if (! $this->tboss_o) - $validator->errors()->add('to',sprintf('Undefined Node [%s] for destination.',$this->tboss)); + if (! $this->fftn) + $validator->errors()->add('from',sprintf('Undefined Node [%s] sent message.',$this->fftn_t)); + if ($this->isNetmail() && (! $this->tftn)) + $validator->errors()->add('to',sprintf('Undefined Node [%s] for destination.',$this->tftn_t)); }); + $this->mo->errors = $validator->errors(); + if ($validator->fails()) - $this->mo->errors = $validator; + Log::debug(sprintf('%s:! Message fails validation (%s@%s->%s@%s)',self::LOGKEY,$this->mo->from,$this->fftn_t,$this->mo->to,$this->tftn_t),['result'=>$validator->errors()]); return $validator; } diff --git a/app/Classes/FTN/Packet.php b/app/Classes/FTN/Packet.php index e08a512..3b3dc55 100644 --- a/app/Classes/FTN/Packet.php +++ b/app/Classes/FTN/Packet.php @@ -11,7 +11,7 @@ use Symfony\Component\HttpFoundation\File\File; use App\Classes\FTN as FTNBase; use App\Exceptions\InvalidPacketException; -use App\Models\{Address,Domain,Echomail,Netmail,Software,Zone}; +use App\Models\{Address,Domain,Echomail,Netmail,Software,System,Zone}; use App\Notifications\Netmails\EchomailBadAddress; /** @@ -206,7 +206,7 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable */ public function __get($key) { - Log::debug(sprintf('%s:/ Requesting key for Packet::class [%s]',self::LOGKEY,$key)); + //Log::debug(sprintf('%s:/ Requesting key for Packet::class [%s]',self::LOGKEY,$key)); switch ($key) { // From Addresses @@ -418,15 +418,13 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable $msg = Message::parseMessage($message,$this->zone); - // @todo If the message from domain (eg: $msg->fftn->zone->domain) is different to the packet address domain ($pkt->fftn->zone->domain), we'll skip this message - Log::debug(sprintf('%s:^ Message [%s] - Packet from domain [%d], Message domain [%d]',self::LOGKEY,$msg->msgid,$this->fftn->zone->domain_id,$msg->fftn->zone->domain_id)); - // If the message is invalid, we'll ignore it - if ($msg->errors) { - Log::info(sprintf('%s:- Message [%s] has errors',self::LOGKEY,$msg->msgid)); + if ($msg->errors->count()) { + Log::info(sprintf('%s:- Message [%s] has [%d] errors',self::LOGKEY,$msg->msgid ?: 'No ID',$msg->errors->count())); + // If the messages is not for the right zone, we'll ignore it - if ($msg->errors->messages()->has('invalid-zone')) { + if ($msg->errors->has('invalid-zone')) { Log::alert(sprintf('%s:! Message [%s] is from an invalid zone [%s], packet is from [%s] - ignoring it',self::LOGKEY,$msg->msgid,$msg->fftn->zone->zone_id,$this->fftn->zone->zone_id)); if (! $msg->rescanned->count()) @@ -435,81 +433,60 @@ abstract class Packet extends FTNBase implements \Iterator, \Countable return; } - // @todo If the $msg->fftn doesnt exist, we'll need to create it - // @todo If the $msg->tftn doesnt exist (netmail), we'll need to create it (ergo intransit) -/* - // If the to address doenst exist, we'll create a new entry - if ($msg->errors->messages()->has('to') && $msg->tzone) { - try { - // @todo Need to work out the correct region for the host_id - Address::unguard(); - $ao = Address::firstOrNew([ - 'zone_id' => $msg->tzone->id, - //'region_id' => 0, - 'host_id' => $msg->tn, - 'node_id' => $msg->tf, - 'point_id' => $msg->tp, - 'active' => TRUE, // @todo This should be false, as it hasnt been assigned to the node - ]); - Address::reguard(); + // If the $msg->fftn doesnt exist, we'll need to create it + if ($msg->errors->has('from') && $this->fftn && $this->fftn->zone_id) { + Log::debug(sprintf('%s:^ From address [%s] doesnt exist, it needs to be created',self::LOGKEY,$msg->set->get('set_fftn'))); - if (is_null($ao->region_id)) - $ao->region_id = $ao->host_id; + $ao = Address::findFTN($msg->set->get('set_fftn'),TRUE); + + if ($ao?->exists && ($ao->zone?->domain_id !== $this->fftn->zone->domain_id)) { + Log::alert(sprintf('%s:! From address [%s] domain [%d] doesnt match packet domain [%d]?',self::LOGKEY,$msg->set->get('set_fftn'),$ao->zone?->domain_id,$this->fftn->zone->domain_id)); - } catch (\Exception $e) { - Log::error(sprintf('%s:! Error finding/creating TO address [%s] for message',self::LOGKEY,$msg->tboss),['error'=>$e->getMessage()]); - $this->errors->push($msg); return; } - $so = System::createUnknownSystem(); + if (! $ao) { + $so = System::createUnknownSystem(); + $ao = Address::createFTN($msg->set->get('set_fftn'),$so); + } - $so->addresses()->save($ao); - - Log::alert(sprintf('%s:- To FTN is not defined, creating new entry for [%s] (%d)',self::LOGKEY,$msg->tboss,$ao->id)); + $msg->fftn_id = $ao->id; + Log::alert(sprintf('%s:- From FTN [%s] is not defined, created new entry for (%d)',self::LOGKEY,$msg->set->get('set_fftn'),$ao->id)); } - // If the from address doenst exist, we'll create a new entry - if ($msg->errors->messages()->has('from') && $msg->tzone) { - try { - // @todo Need to work out the correct region for the host_id - Address::unguard(); - $ao = Address::firstOrNew([ - 'zone_id' => $msg->fzone->id, - //'region_id' => 0, - 'host_id' => $msg->fn, - 'node_id' => $msg->ff, - 'point_id' => $msg->fp, - 'active'=> TRUE, // @todo This should be FALSE as it hasnt been assigned to the node - ]); - Address::reguard(); + // If the $msg->tftn doesnt exist, we'll need to create it + if ($msg->errors->has('to') && $this->tftn && $this->tftn->zone_id) { + Log::debug(sprintf('%s:^ To address [%s] doesnt exist, it needs to be created',self::LOGKEY,$msg->set->get('set_tftn'))); - if (is_null($ao->region_id)) - $ao->region_id = $ao->host_id; + $ao = Address::findFTN($msg->set->get('set_tftn'),TRUE); + + if ($ao?->exists && ($ao->zone?->domain_id !== $this->tftn->zone->domain_id)) { + Log::alert(sprintf('%s:! To address [%s] domain [%d] doesnt match packet domain [%d]?',self::LOGKEY,$msg->set->get('set_tftn'),$ao->zone?->domain_id,$this->fftn->zone->domain_id)); - } catch (\Exception $e) { - Log::error(sprintf('%s:! Error finding/creating FROM address [%s] for message',self::LOGKEY,$msg->fboss),['error'=>$e->getMessage()]); - $this->errors->push($msg); return; } - $so = System::createUnknownSystem(); + if (! $ao) { + $so = System::createUnknownSystem(); + $ao = Address::createFTN($msg->set->get('set_fftn'),$so); + } - $so->addresses()->save($ao); - - Log::alert(sprintf('%s:- From FTN is not defined, creating new entry for [%s] (%d)',self::LOGKEY,$msg->fboss,$ao->id)); + $msg->tftn_id = $ao->id; + Log::alert(sprintf('%s:- To FTN [%s] is not defined, created new entry for (%d)',self::LOGKEY,$msg->set->get('set_tftn'),$ao->id)); } -*/ - // If the from/to user is missing - if ($msg->errors->messages()->has('from') || $msg->errors->messages()->has('to')) { - Log::error(sprintf('%s:! Skipping message [%s] due to errors (%s)...',self::LOGKEY,$msg->msgid,join(',',$msg->errors->messages()->keys()))); - $this->errors->push($msg); + // If there is no fftn, then its from a system that we dont know about + if (! $this->fftn) { + Log::alert(sprintf('%s:! No further message processing, packet is from a system we dont know about [%s]',self::LOGKEY,$this->fftn_t)); + $this->messages->push($msg); return; } } + // @todo If the message from domain (eg: $msg->fftn->zone->domain) is different to the packet address domain ($pkt->fftn->zone->domain), we'll skip this message + Log::debug(sprintf('%s:^ Message [%s] - Packet from domain [%d], Message domain [%d]',self::LOGKEY,$msg->msgid,$this->fftn->zone->domain_id,$msg->fftn->zone->domain_id)); + $this->messages->push($msg); } diff --git a/app/Classes/FTN/Packet/FSC39.php b/app/Classes/FTN/Packet/FSC39.php index 5fb2d36..82a0155 100644 --- a/app/Classes/FTN/Packet/FSC39.php +++ b/app/Classes/FTN/Packet/FSC39.php @@ -63,34 +63,36 @@ final class FSC39 extends Packet */ protected function header(): string { + $oldest = $this->messages->sortBy('datetime')->last(); + try { return pack(collect(self::HEADER)->pluck(1)->join(''), - $this->ff, // Orig Node - $this->tf, // Dest Node - Arr::get($this->header,'y'), // Year - Arr::get($this->header,'m'), // Month - Arr::get($this->header,'d'), // Day - Arr::get($this->header,'H'), // Hour - Arr::get($this->header,'M'), // Minute - Arr::get($this->header,'S'), // Second - 0, // Baud - 2, // Packet Version (should be 2) - $this->fn, // Orig Net - $this->tn, // Dest Net - (Setup::PRODUCT_ID & 0xff), // Product Code Lo - Setup::PRODUCT_VERSION_MAJ, // Product Version Major - $this->password, // Packet Password - $this->fz, // Orig Zone - $this->tz, // Dest Zone - '', // Reserved + $this->fftn_p->node_id, // Orig Node + $this->tftn_p->node_id, // Dest Node + $oldest->datetime->format('Y'), // Year + $oldest->datetime->format('m')-1, // Month + $oldest->datetime->format('d'), // Day + $oldest->datetime->format('H'), // Hour + $oldest->datetime->format('i'), // Minute + $oldest->datetime->format('s'), // Second + 0, // Baud + 2, // Packet Version (should be 2) + $this->fftn_p->host_id, // Orig Net + $this->tftn_p->host_id, // Dest Net + (Setup::PRODUCT_ID & 0xff), // Product Code Lo + Setup::PRODUCT_VERSION_MAJ, // Product Version Major + $this->pass_p ?: $this->tftn_p->session('pktpass'), // Packet Password + $this->fftn_p->zone->zone_id, // Orig Zone + $this->tftn_p->zone->zone_id, // Dest Zone + '', // Reserved Arr::get($this->header,'capvalid',1<<0), // fsc-0039.004 (copy of 0x2c) - ((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi - Setup::PRODUCT_VERSION_MIN, // Product Version Minor - Arr::get($this->header,'capword',1<<0), // Capability Word - $this->fz, // Orig Zone - $this->tz, // Dest Zone - $this->fp, // Orig Point - $this->tp, // Dest Point + ((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi + Setup::PRODUCT_VERSION_MIN, // Product Version Minor + 1<<0, // Capability Word + $this->fftn_p->zone->zone_id, // Orig Zone + $this->tftn_p->zone->zone_id, // Dest Zone + $this->fftn_p->point_id, // Orig Point + $this->tftn_p->point_id, // Dest Point strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData ); diff --git a/app/Classes/FTN/Packet/FSC45.php b/app/Classes/FTN/Packet/FSC45.php index b22c35a..4f719bf 100644 --- a/app/Classes/FTN/Packet/FSC45.php +++ b/app/Classes/FTN/Packet/FSC45.php @@ -44,22 +44,22 @@ final class FSC45 extends Packet { try { return pack(collect(self::HEADER)->pluck(1)->join(''), - $this->ff, // Orig Node - $this->tf, // Dest Node - $this->fp, // Orig Point - $this->tp, // Dest Point - '', // Reserved - 2, // Sub Version (should be 2) - 2, // Packet Version (should be 2) - $this->fn, // Orig Net - $this->tn, // Dest Net - (Setup::PRODUCT_ID & 0xff), // Product Code - Setup::PRODUCT_VERSION_MAJ, // Product Version - $this->password, // Packet Password - $this->fz, // Orig Zone - $this->tz, // Dest Zone - $this->fd, // Orig Domain - $this->td, // Dest Domain + $this->fftn_p->node_id, // Orig Node + $this->tftn_p->node_id, // Dest Node + $this->fp, // Orig Point + $this->tp, // Dest Point + '', // Reserved + 2, // Sub Version (should be 2) + 2, // Packet Version (should be 2) + $this->fftn_p->host_id, // Orig Net + $this->tftn_p->host_id, // Dest Net + (Setup::PRODUCT_ID & 0xff), // Product Code + Setup::PRODUCT_VERSION_MAJ, // Product Version + $this->pass_p ?: $this->tftn_p->session('pktpass'), // Packet Password + $this->fftn_p->zone->zone_id, // Orig Zone + $this->tftn_p->zone->zone_id, // Dest Zone + $this->fftn_p->zone->domain->name, // Orig Domain + $this->tftn_p->zone->domain->name, // Dest Domain strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData ); diff --git a/app/Classes/FTN/Packet/FTS1.php b/app/Classes/FTN/Packet/FTS1.php index ee67f1e..9cb58ab 100644 --- a/app/Classes/FTN/Packet/FTS1.php +++ b/app/Classes/FTN/Packet/FTS1.php @@ -43,26 +43,28 @@ final class FTS1 extends Packet */ protected function header(): string { + $oldest = $this->messages->sortBy('datetime')->last(); + try { return pack(collect(self::HEADER)->pluck(1)->join(''), - $this->ff, // Orig Node - $this->tf, // Dest Node - Arr::get($this->header,'y'), // Year - Arr::get($this->header,'m'), // Month - Arr::get($this->header,'d'), // Day - Arr::get($this->header,'H'), // Hour - Arr::get($this->header,'M'), // Minute - Arr::get($this->header,'S'), // Second - 0, // Baud - 2, // Packet Version (should be 2) - $this->fn, // Orig Net - $this->tn, // Dest Net - (Setup::PRODUCT_ID & 0xff), // Product Code Lo - Setup::PRODUCT_VERSION_MAJ, // Product Version Major - $this->password, // Packet Password - $this->fz, // Orig Zone - $this->tz, // Dest Zone - '', // Reserved + $this->fftn_p->node_id, // Orig Node + $this->tftn_p->node_id, // Dest Node + $oldest->datetime->format('Y'), // Year + $oldest->datetime->format('m')-1, // Month + $oldest->datetime->format('d'), // Day + $oldest->datetime->format('H'), // Hour + $oldest->datetime->format('i'), // Minute + $oldest->datetime->format('s'), // Second + 0, // Baud + 2, // Packet Version (should be 2) + $this->fftn_p->host_id, // Orig Net + $this->tftn_p->host_id, // Dest Net + (Setup::PRODUCT_ID & 0xff), // Product Code Lo + Setup::PRODUCT_VERSION_MAJ, // Product Version Major + $this->pass_p ?: $this->tftn_p->session('pktpass'), // Packet Password + $this->fftn_p->zone->zone_id, // Orig Zone + $this->tftn_p->zone->zone_id, // Dest Zone + '', // Reserved ); } catch (\Exception $e) { diff --git a/app/Console/Commands/PacketInfo.php b/app/Console/Commands/PacketInfo.php index 43bd5a6..7b54526 100644 --- a/app/Console/Commands/PacketInfo.php +++ b/app/Console/Commands/PacketInfo.php @@ -69,7 +69,7 @@ class PacketInfo extends Command try { $this->warn(sprintf('- Date : %s (%s)',$msg->datetime,$msg->datetime->tz->toOffsetName())); - $this->warn(sprintf(' - Errors : %s',$msg->errors?->errors()->count() ? 'YES' : 'No')); + $this->warn(sprintf(' - Errors : %s',$msg->errors->count() ? 'YES' : 'No')); $this->warn(sprintf(' - Flags : %s',$msg->flags()->keys()->join(', '))); $this->warn(sprintf(' - Cost : %d',$msg->cost)); $this->warn(sprintf(' - From : %s (%s)',$msg->from,$msg->fftn->ftn)); @@ -81,10 +81,10 @@ class PacketInfo extends Command if ($msg instanceof Echomail) $this->warn(sprintf(' - Area : %s',$msg->echoarea->name)); - if ($msg->errors) { + if ($msg->errors->count()) { echo "\n"; $this->error("Errors:"); - foreach ($msg->errors->errors()->all() as $error) + foreach ($msg->errors->all() as $error) $this->error(' - '.$error); } @@ -103,7 +103,7 @@ class PacketInfo extends Command $this->error(sprintf(' - To: %s (%s)',$msg->to,$msg->tftn)); $this->error(sprintf(' - Subject: %s',$msg->subject)); - foreach ($msg->errors->errors()->all() as $error) + foreach ($msg->errors->all() as $error) $this->line(' - '.$error); } diff --git a/app/Jobs/PacketProcess.php b/app/Jobs/PacketProcess.php index 0b90cf8..45f2efb 100644 --- a/app/Jobs/PacketProcess.php +++ b/app/Jobs/PacketProcess.php @@ -115,8 +115,8 @@ class PacketProcess implements ShouldQueue elseif ($msg instanceof Echomail) Log::info(sprintf('%s:- Echomail from [%s]',self::LOGKEY,$msg->fftn->ftn)); - if ($msg->errors) { - Log::error(sprintf('%s:! Message [%s] has [%d] errors, unable to process',self::LOGKEY,$msg->msgid,$msg->errors->errors()->count())); + if ($msg->errors->count()) { + Log::error(sprintf('%s:! Message [%s] has [%d] errors, unable to process',self::LOGKEY,$msg->msgid,$msg->errors->count())); continue; } diff --git a/app/Models/Address.php b/app/Models/Address.php index 7af8b01..e3cf3e5 100644 --- a/app/Models/Address.php +++ b/app/Models/Address.php @@ -154,7 +154,7 @@ class Address extends Model $o = new self; $o->active = TRUE; $o->zone_id = $zo->id; - $o->region_id = 0; // @todo Automatically determine region + $o->region_id = $ftn['r']; $o->host_id = $ftn['n']; $o->node_id = $ftn['f']; $o->point_id = $ftn['p']; @@ -307,8 +307,39 @@ class Address extends Model 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 = 0; + $zone_id = NULL; + + // We can only work out region if we have a domain + if ($matches[5] ?? NULL) { + $o = new self; + $o->host_id = $matches[2]; + $o->node_id = $matches[3]; + $o->point_id = empty($matches[4]) ? 0 : (int)$matches[4]; + + $zo = Zone::select('zones.*')->where('zone_id',$matches[1])->join('domains',['domains.id'=>'zones.domain_id'])->where('domains.name',$matches[5])->single(); + $o->zone_id = $zo?->id; + $parent = $o->parent(); + $zone_id = $parent?->zone->zone_id; + + // For flattened domains + if ($zo->domain->flatten && is_null($zone_id)) + foreach ($zo->domain->zones as $zoo) { + $o->zone_id = $zoo->id; + $parent = $o->parent(); + + if ($parent) + break; + } + + $region_id = $parent?->region_id; + $zone_id = $parent?->zone->zone_id; + } + return [ - 'z'=>(int)$matches[1], + '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], diff --git a/app/Models/Echomail.php b/app/Models/Echomail.php index ec59abd..d3b9a60 100644 --- a/app/Models/Echomail.php +++ b/app/Models/Echomail.php @@ -59,6 +59,7 @@ final class Echomail extends Model implements Packet $this->{$key} = $value; break; + case 'set_fftn': // Values that we pass to boot() to record how we got this echomail case 'set_pkt': case 'set_recvtime': diff --git a/app/Models/Netmail.php b/app/Models/Netmail.php index 1ed78c6..a06f83a 100644 --- a/app/Models/Netmail.php +++ b/app/Models/Netmail.php @@ -55,6 +55,8 @@ final class Netmail extends Model implements Packet break; + case 'set_fftn': + case 'set_tftn': // Values that we pass to boot() to record how we got this netmail case 'set_pkt': case 'set_recvtime': diff --git a/app/Traits/MessageAttributes.php b/app/Traits/MessageAttributes.php index f04b38d..466b09e 100644 --- a/app/Traits/MessageAttributes.php +++ b/app/Traits/MessageAttributes.php @@ -8,7 +8,7 @@ namespace App\Traits; use Carbon\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; -use Illuminate\Validation\Validator as ValidatorResult; +use Illuminate\Support\MessageBag; use App\Classes\FTN\Message; use App\Models\Address; @@ -20,7 +20,7 @@ trait MessageAttributes // Items we need to set when creating() public Collection $set; // Validation Errors - public ?ValidatorResult $errors = NULL; + public ?MessageBag $errors = NULL; private const cast_utf8 = [ 'to', @@ -46,7 +46,7 @@ trait MessageAttributes public function getContentAttribute(): string { if ($this->msg_src) - return $this->msg_src."\r"; + return $this->msg_src; // If we have a msg_src attribute, we'll use that $result = $this->msg."\r\r"; @@ -60,7 +60,7 @@ trait MessageAttributes if ($this->origin) $result .= sprintf("%s",$this->origin); - return rtrim($result,"\r")."\r"; + return rtrim($result,"\r"); } public function getDateAttribute(): Carbon diff --git a/resources/views/pkt.blade.php b/resources/views/pkt.blade.php index 32c2669..58858f8 100644 --- a/resources/views/pkt.blade.php +++ b/resources/views/pkt.blade.php @@ -94,7 +94,7 @@