*/ class EsiParserException extends Exception{} /** * Parses ESI kill representations ({@link \Swagger\Client\Model\GetKillmailsKillmailIdKillmailHashOk}) * into kill objects and posts the kill */ class EsiParser { /** @var \Swagger\Client\Model\GetKillmailsKillmailIdKillmailHashOk the ESI kill representation */ protected $EsiKill; /** @var string the crest hash value */ protected $crestHash; protected $externalID = 0; protected $dupeid_ = 0; protected $hash = null; protected $trust = 0; /** @var array an indexed array, using the input IDs as index */ protected $idNameMapping; /** @var boolean isNPCOnly flag indicating the killmail has only NPCs as involved parties */ private $isNPCOnly = true; /** @var boolean allowNpcOnlyKills flag indicating whether killmails with only NPCs as involved parties may be posted */ private $allowNpcOnlyKills = true; /** * Creates and initializes the parser for the given kill Id and hash. * */ function __construct($killId, $hash) { $this->externalID = $killId; $this->crestHash = $hash; } /** * Parses and posts the kill * @return mixed the internal kill ID if posted successfully, false if an error occurs while adding the kill to the database * @throws EsiParserException if there's an error parsing the kill * @throws ApiException if there's an error while communicating with ESI * @throws KillException if there's an error adding the kill */ function parse() { // create killmail representation // get instance try { $this->EsiKill = ESI_Helpers::fetchKill($this->externalID, $this->crestHash); } catch(ApiException $e) { EDKError::log(ESI::getApiExceptionReason($e) . PHP_EOL . $e->getTraceAsString()); throw $e; } // gather all involved entity IDs for bulk translating to names $this->idNameMapping = $this->getEntityIds($this->EsiKill); $timestamp = ESI_Helpers::formatDateTime($this->EsiKill->getKillmailTime()); // Check hashes. $hash = $this->hashMail($this->EsiKill); $trust = null; $kill_id = null; $checkHash = new DBPreparedQuery(); $checkHash->prepare('SELECT kll_id, kll_trust FROM kb3_mails WHERE kll_timestamp = ? AND kll_hash = ?'); $arr = array(&$kill_id, &$trust); $checkHash->bind_results($arr); $types = 'ss'; $arr2 = array(&$types, &$timestamp, &$hash); $checkHash->bind_params($arr2); $checkHash->execute(); if($checkHash->recordCount()) { $checkHash->fetch(); $this->dupeid_ = $kill_id; // We still want to update the external ID if we were given one. if($this->externalID) { $x = null; $y = null; $z = null; $Position = $this->EsiKill->getVictim()->getPosition(); if(!is_null($Position)) { $x = $Position->getX(); $y = $Position->getY(); $z = $Position->getZ(); } // update the kill's coordinates, if the we don't know them already if(!is_null($x) && !is_null($y) && !is_null($z)) { $updateParams = new \DBPreparedQuery(); $updateParams->prepare("UPDATE kb3_kills" ." JOIN kb3_mails ON kb3_mails.kll_id = kb3_kills.kll_id" ." SET kb3_kills.kll_external_id = ?" .", kb3_mails.kll_external_id = ?" .", kll_modified_time = UTC_TIMESTAMP()" .", kb3_kills.kll_x = ?" .", kb3_kills.kll_y = ?" .", kb3_kills.kll_z = ?" ." WHERE kb3_kills.kll_id = ?" ." AND (kb3_kills.kll_external_id IS NULL OR kb3_kills.kll_x = 0)"); $types = 'iidddi'; $arr = array(&$types, &$this->externalID, &$this->externalID, &$x, &$y, &$z, &$this->dupeid_); $updateParams->bind_params($arr); $updateParams->execute(); } // update trust level if($trust >= 0 && $this->trust && $trust > $this->trust) { $updateTrust = new \DBPreparedQuery(); $updateTrust->prepare('UPDATE kb3_mails SET kll_trust = ? WHERE kll_id = ?'); $types = 'ii'; $arr = array(&$types, &$this->trust, &$this->dupeid_); $updateTrust->bind_params($arr); $updateTrust->execute(); } } // we also want to update the CREST hash $updateTrust = new \DBPreparedQuery(); $updateTrust->prepare('UPDATE kb3_mails SET kll_crest_hash = ? WHERE kll_id = ?'); $types = 'si'; $arr = array(&$types, &$this->crestHash, &$this->dupeid_); $updateTrust->bind_params($arr); $updateTrust->execute(); if($trust < 0) { throw new EsiParserException("That mail has been deleted permanently. Kill id was ".$this->getDupeID(), -4); } throw new EsiParserException("That killmail has already been posted getDupeID(), 'kll_id')."\">here.", -1); } // Check external IDs else if($this->externalID) { $checkExternalId = new \DBPreparedQuery(); $checkExternalId->prepare('SELECT kll_id FROM kb3_kills WHERE kll_external_id = ?'); $arr = array(&$kill_id); $checkExternalId->bind_results($arr); $types = 'i'; $arr2 = array(&$types, &$this->externalID); $checkExternalId->bind_params($arr2); $checkExternalId->execute(); if($checkExternalId->recordCount() > 0) { $checkExternalId->fetch(); throw new EsiParserException("That killmail has already been posted here.", -1); } } $this->hash = $hash; // Filtering if(config::get('filter_apply')) { $filterdate = config::get('filter_date'); if (strtotime($timestamp) < $filterdate) { $filterdate = kbdate("j F Y", config::get("filter_date")); throw new EsiParserException("You are not allowed to post killmails older than " .$filterdate, -3); } } // create the kill $Kill = new \Kill(); // set external ID $Kill->setExternalID($this->externalID); // set timestamp $Kill->setTimeStamp($timestamp); // set CREST hash $Kill->setCrestHash($this->crestHash); // handle solarSystem $solarSystemID = $this->EsiKill->getSolarSystemId(); $solarSystem = SolarSystem::getByID($solarSystemID); if (!$solarSystem->getName()) { throw new EsiParserException("Unknown solar system ID: ".$solarSystemID); } $Kill->setSolarSystem($solarSystem); // handle victim details $this->processVictim($Kill); $this->processInvolved($Kill); $this->processItems($Kill); if($this->isNPCOnly && !$this->allowNpcOnlyKills) { throw new EsiParserException("Kill is a loss to NPCs only, but posting NPC kills is not allowed!", -5); } return $Kill->add(); } /** * Calculates the EDK legacy killmail hash for uniquely identifying a kill * @param GetKillmailsKillmailIdKillmailHashOk $EsiKill the ESI kill representation to hash * @return string the killmail hash * @throws EsiParserException if any entity ID cannot be resolved to a name */ public function hashMail($EsiKill = null) { if(is_null($EsiKill)) return false; $involvedParties = $EsiKill->getAttackers(); $Victim = $EsiKill->getVictim(); $invListDamage = array(); foreach($involvedParties AS $Attacker) { $invListDamage[] = $Attacker->getDamageDone(); $involvedPartyName = ""; if(null !== $Attacker->getCharacterId()) { if(!isset($this->idNameMapping[$Attacker->getCharacterId()])) { throw new EsiParserException("Unable to resolve involved party ID ".$Attacker->getCharacterId().", Kill-ID: ".$this->externalID); } $involvedPartyName = $this->idNameMapping[$Attacker->getCharacterId()]; } // use "shipTypeName / corpName" for compatibility with legacy parser mails else { // required for NPCs without corp $corpName = "Unknown"; if(null !== $Attacker->getFactionId()) { // hardcoded workaround for the "Unknown" faction (for sleepers) which is not contained in the SDE, hopefully this can be removed soon! if($Attacker->getFactionId() == 500021) { $corpName = "Unknown"; } else { // try getting the corp from our database $Faction = Cacheable::factory('Faction', $Attacker->getFactionId()); $corpName = $Faction->getName(); } } if(null !== $Attacker->getCorporationId()) { if(!isset($this->idNameMapping[$Attacker->getCorporationId()])) { throw new EsiParserException("Unable to resolve involved party corporation ID ".$Attacker->getCorporationId().", Kill-ID: ".$this->externalID); } $corpName = $this->idNameMapping[$Attacker->getCorporationId()]; } $InvolvedShip = new \Item($Attacker->getShipTypeId()); $involvedPartyName = $InvolvedShip->getName()." / ".$corpName; } if($Attacker->getFinalBlow() === true) { // add the string " (laid the final blow)" to keep compatibility with legacy parser mails $involvedPartyName .= " (laid the final blow)"; } $invListName[] = $involvedPartyName; } // Sort the involved list by damage done then alphabetically. array_multisort($invListDamage, SORT_DESC, SORT_NUMERIC, $invListName, SORT_ASC, SORT_STRING); // timestamp $hashIn = ESI_Helpers::formatDateTime($EsiKill->getKillmailTime()); // cut off seconds from timestamp to keep compatibility with legacy parser mails $hashIn = substr($hashIn, 0, 16); // victim's name // was it a player? if(null !== $Victim->getCharacterId()) { if(!isset($this->idNameMapping[$Victim->getCharacterId()])) { throw new EsiParserException("Unable to resolve victim ID ".$Victim->getCharacterId()); } $hashIn .= $this->idNameMapping[$Victim->getCharacterId()]; } // was it a pos structure? else if(null !== $EsiKill->getMoonId()) { $moonName = \ESI_Helpers::getMoonName($EsiKill->getMoonId()); // cut off the first two characters (again, to keep compatibility with legacy parser killmails) $hashIn .= substr($moonName, 2, strlen($moonName)-1); } else { return false; } // destroyed ship $VictimShip = new \Item($Victim->getShipTypeId()); $hashIn .= $VictimShip->getName(); // solar system $SolarSystem = new \SolarSystem($EsiKill->getSolarSystemId()); $hashIn .= $SolarSystem->getName(); // damage taken $hashIn .= $Victim->getDamageTaken(); // list of involved parties $hashIn .= implode(',', $invListName); // list of involved parties' damage done $hashIn .= implode(',', $invListDamage); return md5($hashIn, true); } /** * @return integer */ public function getDupeID() { return $this->dupeid_; } public function setTrust($trust) { $this->trust = intval($trust); } /** * Sets the flag whether to allow posting of kills containing * only NPCs as involved parties. * @param boolean $allowNpcOnlyKills */ public function setAllowNpcOnlyKills($allowNpcOnlyKills) { $this->allowNpcOnlyKills = (boolean) $allowNpcOnlyKills; } /** * Returns whether posting of kills with only NPCs as involved parties is allowed. * * @return boolean true if posting of NPC only kills is allowed, otherwise false */ public function getAllowNpcOnlyKills() { return $this->allowNpcOnlyKills; } /** * extracts and sets victim details in the given kill * reference; uses $this->killmailRepresentation as source * @param Kill $Kill reference to the kill to update * @throws EsiParserException */ protected function processVictim(&$Kill) { $Victim = $this->EsiKill->getVictim(); $timestamp = \ESI_Helpers::formatDateTime($this->EsiKill->getKillmailTime()); // If we have no character ID and no name then it's a structure or NPC // - if we have a moonID (anchored at a moon) call it corpname - moonname // - if we don't have a moonID call it corpname - systemname if (!$Victim->getCorporationId() && !$Victim->getFactionId()) { throw new EsiParserException("Insufficient victim corpiration information provided! Kill-ID: ".$this->externalID); } $characterId = $Victim->getCharacterId(); $corporationId = $Victim->getCorporationId(); $allianceId = $Victim->getAllianceId(); $factionId = $Victim->getFactionId(); // character ID could not be resolved to a name if($characterId !== null && !isset($this->idNameMapping[$characterId])) { throw new EsiParserException("Unable to resolve victim character ID ".$characterId.", Kill-ID: ".$this->externalID); } // corp ID is present, but could not be resolved to a name if(null !== $corporationId && !isset($this->idNameMapping[$corporationId])) { throw new EsiParserException("Unable to resolve victim corporation ID ".$corporationId.", Kill-ID: ".$this->externalID); } // alliance ID is present, but could not be resolved to a name if(null !== $allianceId && !isset($this->idNameMapping[$allianceId])) { throw new EsiParserException("Unable to resolve victim alliance ID ".$allianceId.", Kill-ID: ".$this->externalID); } // get alliance if(null !== $allianceId) { $Alliance = \Alliance::add($this->idNameMapping[$allianceId], $allianceId); } else if(null !== $factionId) { $Faction = Cacheable::factory('Faction', $factionId); $Alliance = \Alliance::add($Faction->getName(), $factionId); } else { $Alliance = \Alliance::add("None"); } // get corp // if corp is not present, use faction if(null !== $corporationId) { $Corp = \Corporation::add($this->idNameMapping[$corporationId], $Alliance, $timestamp, $corporationId, false); } else if(null !== $factionId) { // try getting the corp from our database $Faction = Cacheable::factory('Faction', $factionId); // harcoded workaround for the "Unknown" faction (for sleepers) which is not contained in the SDE, hopefully this can be removed soon! if($factionId == 500021) { $factionName = "Unknown"; } else { $factionName = $Faction->getName(); } $Corp = Corporation::add($factionName, $Alliance, $timestamp, $factionId, false); } // NPCs without Corp/Alliance/Faction (e.g. Rogue Drones) else { $Corp = Corporation::add("Unknown", $Alliance, $timestamp); } // victim's name if(is_null($characterId)) { if(null !== $this->EsiKill->getMoonId()) { $moonName = \ESI_Helpers::getMoonName($this->EsiKill->getMoonId()); $victimName = $Corp->getName()." - ".$moonName; } else { $victimName = $Corp->getName()." - ".$Kill->getSystem()->getName(); } } if(isset($victimName)) { $Pilot = Pilot::add($victimName, $Corp, $timestamp, $characterId, false); } else { $Pilot = Pilot::add($this->idNameMapping[$characterId], $Corp, $timestamp, $characterId, false); } // handle victim's ship $Ship = Ship::getByID($Victim->getShipTypeId()); // set values in $Kill $Kill->setVictim($Pilot); $Kill->setVictimID($Pilot->getID()); $Kill->setVictimCorpID($Corp->getID()); $Kill->setVictimAllianceID($Alliance->getID()); $Kill->setVictimShip($Ship); $Kill->set('dmgtaken', $Victim->getDamageTaken()); $Position = $Victim->getPosition(); // older kills might not have a position if(!is_null($Position)) { $Kill->setXCoordinate($Position->getX()); $Kill->setYCoordinate($Position->getY()); $Kill->setZCoordinate($Position->getZ()); } } /** * processes and adds all involved parties in the given killmail representation * @param Kill $Kill reference to the kill to update * @throws EsiParserException */ protected function processInvolved(&$Kill) { $involvedParties = $this->EsiKill->getAttackers(); $timestamp = \ESI_Helpers::formatDateTime($this->EsiKill->getKillmailTime()); foreach($involvedParties AS $involvedParty) { // sanity check if (!$involvedParty->getShipTypeId() && !$involvedParty->getWeaponTypeId() && !$involvedParty->getCharacterId()) { throw new EsiParserException("Error processing involved party. Kill-ID: ".$this->externalID); } $characterId = $involvedParty->getCharacterId(); $corporationId = $involvedParty->getCorporationId(); $allianceId = $involvedParty->getAllianceId(); $factionId = $involvedParty->getFactionId(); // character ID could not be resolved to a name if(null !== $characterId && !isset($this->idNameMapping[$characterId])) { throw new EsiParserException("Unable to resolve involved party character ID ".$characterId.", Kill-ID: ".$this->externalID); } // corp ID is present, but could not be resolved to a name if(null !== $corporationId && !isset($this->idNameMapping[$corporationId])) { throw new EsiParserException("Unable to resolve involved party corporation ID ".$corporationId.", Kill-ID: ".$this->externalID); } // alliance ID is present, but could not be resolved to a name if(null !== $allianceId && !isset($this->idNameMapping[$allianceId])) { throw new EsiParserException("Unable to resolve involved party alliance ID ".$allianceId.", Kill-ID: ".$this->externalID); } $isNPC = FALSE; // get involved party's ship if(!$involvedParty->getShipTypeId()) { $Ship = Ship::lookup("Unknown"); } else { $Ship = Ship::getByID($involvedParty->getShipTypeId()); } // get alliance $Alliance = Alliance::add("None"); if (null !== $allianceId) { $Alliance = Alliance::add($this->idNameMapping[$allianceId], $allianceId); } // only use faction as alliance if no corporation is given (faction NPC) else if (null !== $factionId && null !== $corporationId) { $Faction = Cacheable::factory('Faction', $factionId); $Alliance = Alliance::add($Faction->getName(), $factionId); } // get corp // if corp is not present, use faction if(null !== $corporationId) { $Corp = Corporation::add(strval($this->idNameMapping[$corporationId]), $Alliance, $timestamp, $corporationId, false); } else if(null !== $factionId) { // try getting the corp from our database $Faction = Cacheable::factory('Faction', $factionId); // harcoded workaround for the "Unknown" faction (for sleepers) which is not contained in the SDE, hopefully this can be removed soon! if($factionId == 500021) { $factionName = "Unknown"; } else { $factionName = $Faction->getName(); } $Corp = Corporation::add($factionName, $Alliance, $timestamp, $factionId, false); } // NPCs without Corp/Alliance/Faction (e.g. Rogue Drones) else { $Corp = \Corporation::add("Unknown", $Alliance, $timestamp); } // get ship class to determine whether it's a tower and // we need to fetch the alliance via the corp $shipClassID = $Ship->getClass()->getID(); if($shipClassID == 35 // small Tower || $shipClassID == 36 // medium Tower || $shipClassID == 37 // large Tower || $shipClassID == 38 // POS Module || $shipClassID == ShipClass::$SHIP_CLASS_ID_CITADELS) // Citadels { if($Alliance->getName() == "None") { $Alliance = $Corp->getAlliance(); } } // victim's name // Fix for case that involved party is an actual pilot without corp // FoxFour is to blame! if(null !== $characterId && null === $corporationId) { $Pilot = new \Pilot($id, $characterId); $Corp = $Pilot->getCorp(); } // check for NPC if($Corp->isNPCCorp()) { $isNPC = TRUE; } // case if no weapon type ID is given (NPCs/Towers/Structures/...) if(null === $involvedParty->getWeaponTypeId()) { $Weapon = $Ship; if(!$Weapon->getName()) { throw new EsiParserException("Involved party is an NPC with a ship type not found in the database! Kill-ID: ".$this->externalID); } } else { $Weapon = Item::getByID($involvedParty->getWeaponTypeId()); } // special case: // NPC/Tower/other structure if(null === $characterId) { $involvedPartyName = $Corp->getName().' - '.$Weapon->getName(); } // the victim if(!$characterId) { $Pilot = Pilot::add($involvedPartyName, $Corp, $timestamp, $characterId, false); } else { $Pilot = \Pilot::add($this->idNameMapping[$characterId], $Corp, $timestamp, $characterId, false); } // create involvedParty $IParty = new InvolvedParty($Pilot->getID(), $Corp->getID(), $Alliance->getID(), $involvedParty->getSecurityStatus(), $Ship->getID(), $Weapon->getID(), $involvedParty->getDamageDone()); $Kill->addInvolvedParty($IParty); if($involvedParty->getFinalBlow() === TRUE) { $Kill->setFBPilotID($Pilot->getID()); } $this->isNPCOnly = $this->isNPCOnly && $isNPC; } } /** * processes all dropped/destroyed items in that kill * and adds them as Dropped/Destroyed * @param type $Kill the kill to add the items to */ protected function processItems(&$Kill) { $items = $this->EsiKill->getVictim()->getItems(); // TODO implement proper CCP flags! foreach($items AS $EsiItem) { // we use this nested construct for perhaps later changing // the way we process single items and nested items $this->processItem($EsiItem, $Kill); } } /** * Accepts an ESI VictimItem representation (top-level, may have child items) * and adds it and all contained items to the given kill * * @param \Swagger\Client\Model\GetKillmailsKillmailIdKillmailHashOkVictimItems1 $EsiItem * @param Kill $Kill the kill reference */ protected function processItem($EsiItem, &$Kill) { // we will add this item with the given flag, even if it's not in our database // that way, when the database is updated, the item will display correctly $Item = Item::getByID($EsiItem->getItemTypeId()); $location = $EsiItem->getFlag(); $singleton = $EsiItem->getSingleton(); if($EsiItem->getQuantityDropped() > 0) { $Kill->addDroppedItem(new \DestroyedItem($Item, $EsiItem->getQuantityDropped(), $singleton, '', $location)); } if($EsiItem->getQuantityDestroyed()) { $Kill->addDestroyedItem(new \DestroyedItem($Item, $EsiItem->getQuantityDestroyed(), $singleton, '', $location)); } // process container-items // check, if $EsiItem is a root-level item, that may have items inside if(!is_null($EsiItem->getItems()) && count($EsiItem->getItems()) > 0) { foreach($EsiItem->getItems() AS $ItemInContainer) { $this->processContainerItem($ItemInContainer, $Kill, $location); } } } /** * Accepts an ESI VictimItem representation (must not have child items) * and adds it to the given kill using the given parent inventory location * of destroyed items * @param \Swagger\Client\Model\GetKillmailsKillmailIdKillmailHashOkVictimItems $EsiItem * @param Kill $Kill the kill reference * @param int $parentItemLocation the item location of the parent item (for containers) */ protected function processContainerItem($EsiItem, &$Kill, $parentItemLocation) { // we will add this item with the given flag, even if it's not in our database // that way, when the database is updated, the item will display correctly $Item = Item::getByID($EsiItem->getItemTypeId()); $singleton = $EsiItem->getSingleton(); if($EsiItem->getQuantityDropped() > 0) { $Kill->addDroppedItem(new \DestroyedItem($Item, $EsiItem->getQuantityDropped(), $singleton, '', $parentItemLocation)); } if($EsiItem->getQuantityDestroyed()) { $Kill->addDestroyedItem(new \DestroyedItem($Item, $EsiItem->getQuantityDestroyed(), $singleton, '', $parentItemLocation)); } } /** * Gathers the IDs of the victim, all involved parties, their * corporations and alliances. *
* The IDs will be globally unique across entity types. The output * can be used for bulk translating entity IDs to names. * * @param GetKillmailsKillmailIdKillmailHashOk $EsiKill the ESI kill representations to get the entity IDs from * @return int[] an array of entity IDs * @throws ApiException */ protected function getEntityIds($EsiKill) { $characterIds = array(); $corporationIds = array(); $allianceIds = array(); $factionIds = array(); // victim IDs $Victim = $EsiKill->getVictim(); $characterId = $Victim->getCharacterId(); $corporationId = $Victim->getCorporationId(); $allianceId = $Victim->getAllianceId(); $factionId = $Victim->getFactionId(); if(!is_null($characterId) && !in_array($characterId, $characterIds)) $characterIds[] = $characterId; if(!is_null($corporationId) && !in_array($corporationId, $corporationIds)) $corporationIds[] = $corporationId; if(!is_null($allianceId) && !in_array($allianceId, $allianceIds)) $allianceIds[] = $allianceId; if(!is_null($factionId) && !in_array($factionId, $factionIds)) $factionIds[] = $factionId; // involved party IDs $InvolvedParties = $EsiKill->getAttackers(); foreach($InvolvedParties as $InvolvedParty) { $characterId = $InvolvedParty->getCharacterId(); $corporationId = $InvolvedParty->getCorporationId(); $allianceId = $InvolvedParty->getAllianceId(); $factionId = $InvolvedParty->getFactionId(); if(!is_null($characterId) && !in_array($characterId, $characterIds)) $characterIds[] = $characterId; if(!is_null($corporationId) && !in_array($corporationId, $corporationIds)) $corporationIds[] = $corporationId; if(!is_null($allianceId) && !in_array($allianceId, $allianceIds)) $allianceIds[] = $allianceId; if(!is_null($factionId) && !in_array($factionId, $factionIds)) $factionIds[] = $factionId; } // using universe/names endpoint $entityIds = array_merge($characterIds, $corporationIds, $allianceIds); $idToNameMap = ESI_Helpers::resolveEntityIds($entityIds); // now resolve factions foreach($factionIds as $factionId) { $Faction = new Faction($factionId); $idToNameMap[$Faction->getID()] = $Faction->getName(); } return $idToNameMap; } }