crestUrl = $crestUrl; // allow posting of CREST links using the old public-crest base URL $this->crestUrl = str_replace('https://public-crest.eveonline.com', CREST_PUBLIC_URL, $this->crestUrl); $this->crestUrl = preg_replace('#'.preg_quote('https://esi.tech.ccp.is') .'/(v\d|latest)/#', CREST_PUBLIC_URL.'/', $this->crestUrl); } function validateCrestUrl() { // should look like this: // https://crest-tq.eveonline.com/killmails/30290604/787fb3714062f1700560d4a83ce32c67640b1797/ $urlPieces = explode("/", $this->crestUrl); if(count($urlPieces) < 6 || substr($this->crestUrl, 0, strlen(CREST_PUBLIC_URL)) != CREST_PUBLIC_URL || $urlPieces[3] != "killmails" || !is_numeric($urlPieces[4]) || strlen($urlPieces[5]) != 40) { throw new CrestParserException("Invalid CREST URL: ".$this->crestUrl); } } function parse($checkauth = true) { $this->validateCrestUrl(); $urlPieces = explode("/", $this->crestUrl); $this->externalID = (int)$urlPieces[4]; $this->crestHash = $urlPieces[5]; // create killmail representation // get instance try { $this->killmailRepresentation = SimpleCrest::getReferenceByUrl($this->crestUrl); } catch(Exception $e) { throw new CrestParserException($e->getMessage(), $e->getCode()); } $qry = DBFactory::getDBQuery(); // Check hashes with a prepared query. // Make it static so we can reuse the same query for feed fetches. static $timestamp; static $checkHash; static $hash; static $trust; static $kill_id; $timestamp = str_replace('.', '-', $this->killmailRepresentation->killTime); // Check hashes. $hash = self::hashMail($this->killmailRepresentation); if(!isset($checkHash)) { $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) { $victimDetails = self::getVictim($this->killmailRepresentation); $qry->execute("UPDATE kb3_kills" ." JOIN kb3_mails ON kb3_mails.kll_id = kb3_kills.kll_id" ." SET kb3_kills.kll_external_id = ".$this->externalID .", kb3_mails.kll_external_id = ".$this->externalID .", kll_modified_time = UTC_TIMESTAMP()" .", kb3_kills.kll_x = ".$victimDetails["x"] .", kb3_kills.kll_y = ".$victimDetails["y"] .", kb3_kills.kll_z = ".$victimDetails["z"] ." WHERE kb3_kills.kll_id = ".$this->dupeid_ ." AND (kb3_kills.kll_external_id IS NULL OR kb3_kills.kll_x = 0)"); if($trust >= 0 && $this->trust && $trust > $this->trust) { $qry->execute("UPDATE kb3_mails SET kll_trust = " .$this->trust." WHERE kll_id = ".$this->dupeid_); } } // we also want to update the CREST hash $qry->execute("UPDATE kb3_mails SET kll_crest_hash = '" .$this->crestHash."' WHERE kll_id = ".$this->dupeid_); if($trust < 0) { throw new CrestParserException("That mail has been deleted. Kill id was " .$this->getDupeID(), -4); } throw new CrestParserException("That killmail has already been posted getDupeID(), 'kll_id') ."\">here.", -1); } // Check external IDs else if($this->externalID) { $qry->execute('SELECT kll_id FROM kb3_kills WHERE kll_external_id = '.$this->externalID); if($qry->recordCount()) { $row = $qry->getRow(); throw new CrestParserException("That killmail has already been posted here.", -1); } } $this->hash = $hash; // get timestamp $timestamp = $this->killmailRepresentation->killTime; // Filtering if(config::get('filter_apply')) { $filterdate = config::get('filter_date'); if ($timestamp < $filterdate) { $filterdate = kbdate("j F Y", config::get("filter_date")); throw new CrestParserException("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 = (int)$this->killmailRepresentation->solarSystem->id; $solarSystem = SolarSystem::getByID($solarSystemID); if (!$solarSystem->getName()) { throw new CrestParserException("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 CrestParserException("Kill is a loss to NPCs only, but posting NPC kills is not allowed!", -5); } return $Kill->add(); } function error($message, $debugtext = null) { $this->error_[] = array($message, $debugtext); } function getError() { if (count($this->error_)) { return $this->error_; } return false; } /** * * @param mixed $mailRepresentation * @return string */ public static function hashMail($mailRepresentation = null) { if(is_null($mailRepresentation)) return false; $involvedParties = self::getAttackers($mailRepresentation); $victim = self::getVictim($mailRepresentation); $invListDamage = array(); foreach($involvedParties AS $attacker) { $invListDamage[] = $attacker["damageDone"]; // TODO check for NPCs/POSs etc $involvedPartyName = ""; if($attacker["characterName"]) { $involvedPartyName = $attacker["characterName"]; } // use "shipTypeName / corpName" for compatibility with legacy parser mails else { // required for NPCs without corp $corpName = "Unknown"; if(strlen($attacker["factionName"]) > 0) { $corpName = $attacker["factionName"]; } if(strlen($attacker["corporationName"]) > 0) { $corpName = $attacker["corporationName"]; } $involvedPartyName = $attacker["shipTypeName"]." / ".$corpName; } if($attacker["finalBlow"] === 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 = str_replace('.', '-', $mailRepresentation->killTime); // 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($victim["characterName"]) { $hashIn .= $victim["characterName"]; } // was it a pos structure? else if($victim["moonName"]) { // cut off the first two characters (again, to keep compatibility with legacy parser killmails) $hashIn .= substr($victim["moonName"], 2, strlen($victim["moonName"])-1); } else { return false; } // destroyed ship $hashIn .= $victim["shipTypeName"]; // solar system $hashIn .= (String) $mailRepresentation->solarSystem->name; // damage taken $hashIn .= $victim["damageTaken"]; // 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; } /** * @param mixed $mailRepresentation * @return array */ public static function getAttackers($mailRepresentation) { $attackers = array(); foreach($mailRepresentation->attackers as $attacker) { $involvedParty = array(); $involvedParty["characterID"] = (int) @$attacker->character->id; $involvedParty["characterName"] = (string) @$attacker->character->name; $involvedParty["corporationID"] = (int) @$attacker->corporation->id; $involvedParty["corporationName"] = (string) @$attacker->corporation->name; $involvedParty["allianceID"] = (int) @$attacker->alliance->id; $involvedParty["allianceName"] = (string) @$attacker->alliance->name; $involvedParty["factionID"] = (int) @$attacker->faction->id; $involvedParty["factionName"] = (string) @$attacker->faction->name; $involvedParty["securityStatus"] = (float) $attacker->securityStatus; $involvedParty["damageDone"] = (int) @$attacker->damageDone; $involvedParty["finalBlow"] = (boolean) @$attacker->finalBlow; $involvedParty["weaponTypeID"] = (int) @$attacker->weaponType->id; $involvedParty["shipTypeID"] = (int) @$attacker->shipType->id; $involvedParty["shipTypeName"] = (string) @$attacker->shipType->name; $attackers[] = $involvedParty; } return $attackers; } /** * @param mixed $mailRepresentation * @return array */ public static function getVictim($mailRepresentation) { $victim = array(); $victim["shipTypeID"] = (int) @$mailRepresentation->victim->shipType->id; $victim["shipTypeName"] = (string) @$mailRepresentation->victim->shipType->name; $victim["characterID"] = (int) @$mailRepresentation->victim->character->id; $victim["characterName"] = (string) @$mailRepresentation->victim->character->name; $victim["corporationID"] = (int) @$mailRepresentation->victim->corporation->id; $victim["corporationName"] = (string) @$mailRepresentation->victim->corporation->name; $victim["allianceID"] = (int) @$mailRepresentation->victim->alliance->id; $victim["allianceName"] = (string) @$mailRepresentation->victim->alliance->name; $victim["factionID"] = (int) @$mailRepresentation->victim->faction->id; $victim["factionName"] = (string) @$mailRepresentation->victim->faction->name; $victim["damageTaken"] = (int) @$mailRepresentation->victim->damageTaken; $victim["moonName"] = (string) @$mailRepresentation->moon->name; $victim["moonID"] = (int) @$mailRepresentation->moon->id; $victim["x"] = (float) @$mailRepresentation->victim->position->x; $victim["y"] = (float) @$mailRepresentation->victim->position->y; $victim["z"] = (float) @$mailRepresentation->victim->position->z; return $victim; } /** * @param mixed $mailRepresentation * @return array */ private static function getItems($itemsInMail) { $items = array(); if($itemsInMail) { foreach($itemsInMail as $item) { $itemDetails = array(); $itemDetails["typeID"] = (int) @$item->itemType->id; $itemDetails["flag"] = (int) @$item->flag; $itemDetails["qtyDropped"] = (int) @$item->quantityDropped; $itemDetails["qtyDestroyed"] = (int) @$item->quantityDestroyed; $itemDetails["singleton"] = (int) @$item->singleton; // recursive call for containers -> we preserve the item tree here if (isset($item->items)) { $itemDetails["items"] = self::getItems($item->items); } $items[] = $itemDetails; } } return $items; } /** * 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 CrestParserException */ protected function processVictim(&$Kill) { $victimDetails = self::getVictim($this->killmailRepresentation); $timestamp = $this->killmailRepresentation->killTime; // If we have a character ID but no name then we give up - the needed // info is gone. // 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 (!strlen($victimDetails['characterName']) && $victimDetails['characterID'] > 0) { throw new CrestParserException("Insufficient victim information provided! Kill-ID: ".$this->externalID); } else if (!$victimDetails['corporationID'] && !$victimDetails['factionID']) { throw new CrestParserException("Insufficient victim corpiration information provided! Kill-ID: ".$this->externalID); } // get alliance if ($victimDetails['allianceID'] > 0) { $Alliance = Alliance::add($victimDetails['allianceName'], $victimDetails['allianceID']); } else if ($victimDetails['factionID'] > 0) { $Alliance = Alliance::add($victimDetails['factionName'],$victimDetails['factionID']); } else { $Alliance = Alliance::add("None"); } // get corp // if corp is not present, use faction if($victimDetails['corporationID'] > 0) { $Corp = Corporation::add(strval($victimDetails['corporationName']), $Alliance, $timestamp, (int)$victimDetails['corporationID']); } else { $Corp = Corporation::add(strval($victimDetails['factionName']), $Alliance, $timestamp, (int)$victimDetails['factionID']); } // victim's name if(strlen($victimDetails["characterName"]) == 0) { if($victimDetails["moonID"] > 0) { $victimName = $Corp->getName()." - ".$victimDetails["moonName"]; } else { $victimName = $Corp->getName()." - ".$Kill->getSystem()->getName(); } } else { $victimName = $victimDetails["characterName"]; } $Pilot = $pilot = Pilot::add($victimName, $Corp, $timestamp, $victimDetails["characterID"]); // handle victim's ship $Ship = Ship::getByID($victimDetails["shipTypeID"]); // 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', $victimDetails['damageTaken']); $Kill->setXCoordinate($victimDetails['x']); $Kill->setYCoordinate($victimDetails['y']); $Kill->setZCoordinate($victimDetails['z']); } /** * processes and adds all involved parties in the given killmail representation * @param Kill $Kill reference to the kill to update * @throws CrestParserException */ protected function processInvolved(&$Kill) { $involvedParties = self::getAttackers($this->killmailRepresentation); $timestamp = $this->killmailRepresentation->killTime; foreach($involvedParties AS $involvedParty) { if (!$involvedParty['shipTypeID'] && !$involvedParty['weaponTypeID'] && !$involvedParty['characterID'] && !strlen($involvedParty['characterName'])) { throw new CrestParserException("Error processing involved party. Kill-ID: ".$this->externalID); } $isNPC = FALSE; // get involved party's ship $Ship = new Ship(); if(!$involvedParty['shipTypeID']) { $Ship = Ship::lookup("Unknown"); } else { $Ship = Ship::getByID($involvedParty['shipTypeID']); } $Weapon = Cacheable::factory('Item', $involvedParty['weaponTypeID']); // get alliance $Alliance = Alliance::add("None"); if ($involvedParty['allianceID'] > 0) { $Alliance = Alliance::add($involvedParty['allianceName'], $involvedParty['allianceID']); } // only use faction as alliance if no corporation is given (faction NPC) else if ($involvedParty['factionID'] > 0 && strlen($involvedParty['corporationName']) > 0) { $Alliance = Alliance::add($involvedParty['factionName'], $involvedParty['factionID']); } // get corp // if corp is not present, use faction if($involvedParty['corporationID'] > 0) { // try getting the corp from our database $Corp = Corporation::lookup(strval($involvedParty['corporationName'])); // create new corp if(!$Corp) { $Corp = Corporation::add(strval($involvedParty['corporationName']), $Alliance, $timestamp, (int)$involvedParty['corporationID']); } } else if($involvedParty['factionID'] > 0) { // try getting the corp from our database $Corp = Corporation::lookup(strval($involvedParty['factionName'])); // create new corp if(!$Corp) { $Corp = Corporation::add(strval($involvedParty['factionName']), $Alliance, $timestamp, (int)$involvedParty['factionID']); } } // NPCs without Corp/Alliance/Faction (e.g. Rogue Drones) else { $Corp = self::fetchCorp("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 $involvedPartyName = $involvedParty['characterName']; $involvedCharacterID = $involvedParty['characterID']; $loadPilotExternals = true; // Fix for case that involved party is an actual pilot without corp // FoxFour is to blame! if($involvedCharacterID && strlen($involvedParty['characterName']) > 0 && $involvedParty['corporationID'] == 0) { $Pilot = Pilot::lookup($involvedParty['characterName']); if($Pilot) { $Corp = $Pilot->getCorp(); } } // special case: // NPC/Tower/other structure if(!$involvedCharacterID && !$involvedParty['weaponTypeID'] && !$involvedParty['allianceID']) { $Alliance = $Corp->getAlliance(); $Ship = Ship::getByID($involvedParty['shipTypeID']); $Weapon = Item::getByID($involvedParty['shipTypeID']); if(!$Weapon->getName()) { throw new CrestParserException("Involved party is an NPC with a ship type not found in the database! Kill-ID: ".$killData->killID); } $involvedPartyName = $Corp->getName().' - '.$Weapon->getName(); // citadels are no NPCs! if($Ship->getClass()->getID() != ShipClass::$SHIP_CLASS_ID_CITADELS) { $isNPC = TRUE; } $involvedCharacterID = 0; $loadPilotExternals = false; } $Pilot = Pilot::add($involvedPartyName, $Corp, $timestamp, $involvedCharacterID, $loadPilotExternals); // create involvedParty $IParty = new InvolvedParty($Pilot->getID(), $Corp->getID(), $Alliance->getID(), $involvedParty['securityStatus'], $Ship->getID(), $Weapon->getID(), $involvedParty['damageDone']); $Kill->addInvolvedParty($IParty); if($involvedParty["finalBlow"] === 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 = self::getItems($this->killmailRepresentation->victim->items); // TODO implement proper CCP flags! foreach($items AS $item) { // we use this nested construct for perhaps later changing // the way we process single items and nested items $this->processItem($item, $Kill); } } /** * accepts an array with item information, * and adds items to the given kill * of destroyed items * @param array $item * -typeID * -flag * -qtyDropped * -qtyDestroyed * -singleton * @param Kill $Kill the kill reference * @param int $parentItemLocation the item location of the parent item (for containers) */ protected function processItem($item, &$Kill, $parentItemLocation = null) { $typeID = (int)$item['typeID']; // 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($typeID); // if item has a parent, use the parent's flag if(!is_null($parentItemLocation)) { $location = $parentItemLocation; } else { $location = (int)$item['flag']; } // Blueprint copy - in the cargohold // overrides all other locations $singleton = (int)$item['singleton']; if($item['qtyDropped']) { $Kill->addDroppedItem( new DestroyedItem($Item, $item['qtyDropped'], $singleton, '', $location)); } if($item['qtyDestroyed']) { $Kill->addDestroyedItem( new DestroyedItem($Item, $item['qtyDestroyed'], $singleton, '', $location)); } // process container-items if(isset($item["items"])) { foreach($item["items"] AS $itemInContainer) { $this->processItem($itemInContainer, $Kill, $location); } } } /** * Return corporation from cached list or look up a new name. * * @param string $corpName Corp name to look up. * @return Corporation Corporation object matching input name. */ private static function fetchCorp($corpName, $Alliance = null, $timestamp = null) { $corp = Corporation::lookup($corpName); if (!$corp) { if ($Alliance == null) { // If the corporation is new and the alliance unknown (structure) // fetch the alliance from the API. $corp = Corporation::add($corpName, Alliance::add("None"), $timestamp); if (!$corp->getExternalID()) { $corp = false; } else { $corp->execQuery(); } } else { $corp = Corporation::add($corpName, $Alliance, $timestamp, 0, FALSE); } } return $corp; } }