diff --git a/env.sample.php b/env.sample.php index 6e02535..4ea91a4 100644 --- a/env.sample.php +++ b/env.sample.php @@ -46,6 +46,8 @@ $SETTINGS = [ // MaxMind GeoIP2 database "geoip_database" => __DIR__ . "/resources/net.geoip/GeoLite2-City.mmdb", + "usps_client_id" => "", + "usps_client_secret" => "", "usps_user_id" => "", "usps_source_id" => "", "ups_access_key" => "", diff --git a/lib/Tracking_USPS.lib.php b/lib/Tracking_USPS.lib.php index a60fe3c..a595321 100644 --- a/lib/Tracking_USPS.lib.php +++ b/lib/Tracking_USPS.lib.php @@ -24,100 +24,107 @@ class Tracking_USPS { $barcode = new TrackingBarcode($code); try { - $track = new \SimpleXMLElement(""); - $track->addAttribute('USERID', env("usps_user_id")); - $track->addChild('Revision', '1'); - $track->addChild('ClientIp', $_SERVER['REMOTE_ADDR']); - $track->addChild('SourceId', env("usps_source_id", "selfhosted-opensource-default-value.netsyms.net")); - $pack = $track->addChild('TrackID'); - $pack->addAttribute('ID', $barcode->getSanitized()); - $url = 'https://secure.shippingapis.com/ShippingApi.dll?API=TrackV2&XML=' . $track->asXML(); - $xml = simplexml_load_file($url); - if ($xml->getName() == "Error") { - if (!empty($xml->Description)) { - throw new TrackingException("The USPS tracking system is having problems: \"" . trim($xml->Description) . "\""); + $resp = USPSAPIs::getAPIRequest("tracking/v3/tracking/$code?expand=DETAIL"); + $resp = str_replace("reg;", "®", $resp); + $resp = str_replace("®", "®", $resp); + $json = json_decode($resp, true); + + if (!empty($json["error"])) { + if (!empty($json["error"]["errors"]) && $json["error"]["errors"][0]["code"] == "150001") { + // Tracking number not found + throw new TrackingException(str_replace("12: ", "", $json["error"]["errors"][0]["title"])); + } + if (!empty($json["error"]["message"])) { + throw new TrackingException("The USPS tracking system is having problems: \"" . trim($json["error"]["message"]) . "\""); } throw new TrackingException("The USPS tracking system is having problems. Try again later."); } - if (!empty($xml->TrackInfo)) { - $trackinfo = $xml->TrackInfo; - } - if (!empty($xml->TrackInfo->Error)) { - throw new TrackingException(str_replace("®", "®", (string) $xml->TrackInfo->Error->Description)); - } + $trackinfo = $json; } catch (TrackingException $ex) { throw $ex; } catch (Exception $ex) { - throw new TrackingException("There was a server problem. This code cannot be tracked right now."); + throw new TrackingException("There was a server error. This code cannot be tracked right now. Try again later."); } $info = new TrackingInfo(); try { - $info->setCode($trackinfo->attributes()["ID"]); + $info->setCode($trackinfo["trackingNumber"]); } catch (Exception $ex) { throw new TrackingException("The USPS tracking system returned an invalid response. Try again later."); } $info->setCarrier("usps"); - $info->setService(new Service((string) $trackinfo->ClassofMailCode, (string) $trackinfo->Class)); + $info->setService(new Service((string) $trackinfo["mailClass"], (string) $trackinfo["mailClass"])); $info->setCarrierAttributionText(CarrierAssets::getAttribution(Carriers::getCarrierCode($info->getCarrier()))); $info->setCarrierLogo(CarrierAssets::getLogo(Carriers::getCarrierCode($info->getCarrier()))); - $current_status = new TrackingEntry( - TrackingStatus::USPSEventCodeToStatus($trackinfo->TrackSummary->EventCode), - ($trackinfo->StatusSummary ?? "Unknown") . (TrackingStatus::USPSEventCodeToStatus($trackinfo->TrackSummary->EventCode) == TrackingStatus::TRACKING_STATUS_UNKNOWN ? " " . $trackinfo->TrackSummary->EventCode : ""), - $trackinfo->TrackSummary->EventDate . " " . $trackinfo->TrackSummary->EventTime, - null, - TrackingStatus::isUSPSEventCodeContainerScan($trackinfo->TrackSummary->EventCode) - ); + // Current status + if (count($trackinfo["trackingEvents"]) > 0) { + $index = 0; + $evt = $trackinfo["trackingEvents"][$index]; + $current_status = new TrackingEntry( + TrackingStatus::USPSEventCodeToStatus($evt["eventCode"]), + ($evt["eventType"] ?? "Unknown") . (TrackingStatus::USPSEventCodeToStatus($evt["eventCode"]) == TrackingStatus::TRACKING_STATUS_UNKNOWN ? " " . $evt["eventCode"] : ""), + date("Y-m-d\TH:i:s", strtotime($evt["eventTimestamp"])), + null, + TrackingStatus::isUSPSEventCodeContainerScan($evt["eventCode"]) + ); - $current_location = new Location(); - $current_location->city = (string) $trackinfo->TrackSummary->EventCity ?? ""; - $current_location->state = (string) $trackinfo->TrackSummary->EventState ?? ""; - $current_location->zip = (string) $trackinfo->TrackSummary->EventZIPCode ?? ""; - $current_location->country = (string) $trackinfo->TrackSummary->EventCountry ?? ""; + $current_location = new Location(); + $current_location->city = (string) $evt["eventCity"] ?? ""; + $current_location->state = (string) $evt["eventState"] ?? ""; + $current_location->zip = (string) $evt["eventZIP"] ?? ""; + $current_location->country = (string) $evt["eventCountry"] ?? ""; - /* - * Fill in state from list above when it's missing from the API response - */ - if ($current_location->state == "" && $current_location->zip == "") { - if (array_key_exists(strtoupper($current_location->city), self::STATELESS_CITIES)) { - $current_location->state = self::STATELESS_CITIES[$current_location->city]; + /* + * Fill in state from list above when it's missing from the API response + */ + if ($current_location->state == "" && $current_location->zip == "") { + if (array_key_exists(strtoupper($current_location->city), self::STATELESS_CITIES)) { + $current_location->state = self::STATELESS_CITIES[$current_location->city]; + } } + } else { + $current_status = new TrackingEntry( + TrackingStatus::TRACKING_STATUS_PRE_TRANSIT, + ($trackinfo["statusSummary"] ?? "Unknown"), + date("Y-m-d\TH:i:s", strtotime("now")), + null, + false + ); + $current_location = new Location(); } $current_status->setLocation($current_location); $info->setCurrentStatus($current_status); - // USPS doesn't put the latest entry in the history - $info->appendHistoryEntry($current_status); $from = new Location(); - $from->city = (string) $trackinfo->OriginCity ?? ""; - $from->state = (string) $trackinfo->OriginState ?? ""; - $from->zip = (string) $trackinfo->OriginZip ?? ""; - $from->country = (string) $trackinfo->OriginCountryCode ?? ""; + $from->city = (string) $trackinfo["originCity"] ?? ""; + $from->state = (string) $trackinfo["originState"] ?? ""; + $from->zip = (string) $trackinfo["originZIP"] ?? ""; + $from->country = (string) (isset($trackinfo["originCountry"]) ? $trackinfo["originCountry"] : ""); $info->setFrom($from); $to = new Location(); - $to->city = (string) $trackinfo->DestinationCity ?? ""; - $to->state = (string) $trackinfo->DestinationState ?? ""; - $to->zip = (string) $trackinfo->DestinationZip ?? ""; - $to->country = (string) $trackinfo->DestinationCountryCode ?? ""; + $to->city = (string) $trackinfo["destinationCity"] ?? ""; + $to->state = (string) $trackinfo["destinationState"] ?? ""; + $to->zip = (string) $trackinfo["destinationZIP"] ?? ""; + $to->country = (string) (isset($trackinfo["destinationCountry"]) ? $trackinfo["destinationCountry"] : ""); $info->setTo($to); - for ($i = 0; $i < count($trackinfo->TrackDetail); $i++) { - $history = $trackinfo->TrackDetail[$i]; + for ($i = 0; $i < count($trackinfo["trackingEvents"]); $i++) { + $history = $trackinfo["trackingEvents"][$i]; $location = new Location(); - $location->city = (string) $history->EventCity ?? ""; - $location->state = (string) $history->EventState ?? ""; - $location->zip = (string) $history->EventZIPCode ?? ""; - $location->country = (string) $history->EventCountry ?? ""; + $location->city = (string) $history["eventCity"] ?? ""; + $location->state = (string) $history["eventState"] ?? ""; + $location->zip = (string) $history["eventZIP"] ?? ""; + $location->country = (string) $history["eventCountry"] ?? ""; /* * Fill in state from list above when it's missing from the API response */ @@ -126,25 +133,14 @@ class Tracking_USPS { $location->state = self::STATELESS_CITIES[$location->city]; } } - if ((empty($history->EventDate) || empty($history->EventTime)) && $i < count($trackinfo->TrackDetail) - 1) { - // If there's no date/time for some reason (yes this happens sometimes apparently), - // just make it 60 seconds after the previous event. - // This way it'll be in the correct order if the events are displayed - // after being sorted by date/time. - // Because events are ordered latest first, we get $i+1 to get the previous event. - $datetime = date("Y-m-d H:i:s", strtotime($trackinfo->TrackDetail[$i + 1]->EventDate . " " . $trackinfo->TrackDetail[$i + 1]->EventTime) + 60); - } else { - $datetime = $history->EventDate . " " . $history->EventTime; - } $info->appendHistoryEntry(new TrackingEntry( - TrackingStatus::USPSEventCodeToStatus((string) $history->EventCode), - ((string) $history->Event) . (TrackingStatus::USPSEventCodeToStatus((string) $history->EventCode) == TrackingStatus::TRACKING_STATUS_UNKNOWN ? " " . (string) $history->EventCode : ""), - $datetime, + TrackingStatus::USPSEventCodeToStatus((string) $history["eventCode"]), + $history["eventType"] . (TrackingStatus::USPSEventCodeToStatus((string) $history["eventCode"]) == TrackingStatus::TRACKING_STATUS_UNKNOWN ? " " . (string) $history["eventCode"] : ""), + $history["eventTimestamp"], $location, - TrackingStatus::isUSPSEventCodeContainerScan((string) $history->EventCode))); + TrackingStatus::isUSPSEventCodeContainerScan((string) $history["eventCode"]))); } return $info; } - } diff --git a/lib/Tracking_USPS.lib.v2.php b/lib/Tracking_USPS.lib.v2.php new file mode 100644 index 0000000..a60fe3c --- /dev/null +++ b/lib/Tracking_USPS.lib.v2.php @@ -0,0 +1,150 @@ + state + * so the API can return a more complete response. + */ + public const STATELESS_CITIES = [ + "LOS ANGELES" => "CA", + "SAN FRANCISCO" => "CA", + "NEW YORK" => "NY", + "CHICAGO" => "IL", + "MIAMI" => "FL" + ]; + + /** + * + * @param string $code + * @return \TrackingInfo + * @throws TrackingException + */ + public static function track(string $code, string $carrier = ""): TrackingInfo { + $barcode = new TrackingBarcode($code); + + try { + $track = new \SimpleXMLElement(""); + $track->addAttribute('USERID', env("usps_user_id")); + $track->addChild('Revision', '1'); + $track->addChild('ClientIp', $_SERVER['REMOTE_ADDR']); + $track->addChild('SourceId', env("usps_source_id", "selfhosted-opensource-default-value.netsyms.net")); + $pack = $track->addChild('TrackID'); + $pack->addAttribute('ID', $barcode->getSanitized()); + $url = 'https://secure.shippingapis.com/ShippingApi.dll?API=TrackV2&XML=' . $track->asXML(); + $xml = simplexml_load_file($url); + + if ($xml->getName() == "Error") { + if (!empty($xml->Description)) { + throw new TrackingException("The USPS tracking system is having problems: \"" . trim($xml->Description) . "\""); + } + throw new TrackingException("The USPS tracking system is having problems. Try again later."); + } + + if (!empty($xml->TrackInfo)) { + $trackinfo = $xml->TrackInfo; + } + if (!empty($xml->TrackInfo->Error)) { + throw new TrackingException(str_replace("®", "®", (string) $xml->TrackInfo->Error->Description)); + } + } catch (TrackingException $ex) { + throw $ex; + } catch (Exception $ex) { + throw new TrackingException("There was a server problem. This code cannot be tracked right now."); + } + + $info = new TrackingInfo(); + + try { + $info->setCode($trackinfo->attributes()["ID"]); + } catch (Exception $ex) { + throw new TrackingException("The USPS tracking system returned an invalid response. Try again later."); + } + + $info->setCarrier("usps"); + $info->setService(new Service((string) $trackinfo->ClassofMailCode, (string) $trackinfo->Class)); + $info->setCarrierAttributionText(CarrierAssets::getAttribution(Carriers::getCarrierCode($info->getCarrier()))); + $info->setCarrierLogo(CarrierAssets::getLogo(Carriers::getCarrierCode($info->getCarrier()))); + + $current_status = new TrackingEntry( + TrackingStatus::USPSEventCodeToStatus($trackinfo->TrackSummary->EventCode), + ($trackinfo->StatusSummary ?? "Unknown") . (TrackingStatus::USPSEventCodeToStatus($trackinfo->TrackSummary->EventCode) == TrackingStatus::TRACKING_STATUS_UNKNOWN ? " " . $trackinfo->TrackSummary->EventCode : ""), + $trackinfo->TrackSummary->EventDate . " " . $trackinfo->TrackSummary->EventTime, + null, + TrackingStatus::isUSPSEventCodeContainerScan($trackinfo->TrackSummary->EventCode) + ); + + $current_location = new Location(); + $current_location->city = (string) $trackinfo->TrackSummary->EventCity ?? ""; + $current_location->state = (string) $trackinfo->TrackSummary->EventState ?? ""; + $current_location->zip = (string) $trackinfo->TrackSummary->EventZIPCode ?? ""; + $current_location->country = (string) $trackinfo->TrackSummary->EventCountry ?? ""; + + /* + * Fill in state from list above when it's missing from the API response + */ + if ($current_location->state == "" && $current_location->zip == "") { + if (array_key_exists(strtoupper($current_location->city), self::STATELESS_CITIES)) { + $current_location->state = self::STATELESS_CITIES[$current_location->city]; + } + } + + $current_status->setLocation($current_location); + + $info->setCurrentStatus($current_status); + // USPS doesn't put the latest entry in the history + $info->appendHistoryEntry($current_status); + + $from = new Location(); + $from->city = (string) $trackinfo->OriginCity ?? ""; + $from->state = (string) $trackinfo->OriginState ?? ""; + $from->zip = (string) $trackinfo->OriginZip ?? ""; + $from->country = (string) $trackinfo->OriginCountryCode ?? ""; + + $info->setFrom($from); + + $to = new Location(); + $to->city = (string) $trackinfo->DestinationCity ?? ""; + $to->state = (string) $trackinfo->DestinationState ?? ""; + $to->zip = (string) $trackinfo->DestinationZip ?? ""; + $to->country = (string) $trackinfo->DestinationCountryCode ?? ""; + + $info->setTo($to); + + for ($i = 0; $i < count($trackinfo->TrackDetail); $i++) { + $history = $trackinfo->TrackDetail[$i]; + $location = new Location(); + $location->city = (string) $history->EventCity ?? ""; + $location->state = (string) $history->EventState ?? ""; + $location->zip = (string) $history->EventZIPCode ?? ""; + $location->country = (string) $history->EventCountry ?? ""; + /* + * Fill in state from list above when it's missing from the API response + */ + if ($location->state == "" && $location->zip == "") { + if (array_key_exists(strtoupper($location->city), self::STATELESS_CITIES)) { + $location->state = self::STATELESS_CITIES[$location->city]; + } + } + if ((empty($history->EventDate) || empty($history->EventTime)) && $i < count($trackinfo->TrackDetail) - 1) { + // If there's no date/time for some reason (yes this happens sometimes apparently), + // just make it 60 seconds after the previous event. + // This way it'll be in the correct order if the events are displayed + // after being sorted by date/time. + // Because events are ordered latest first, we get $i+1 to get the previous event. + $datetime = date("Y-m-d H:i:s", strtotime($trackinfo->TrackDetail[$i + 1]->EventDate . " " . $trackinfo->TrackDetail[$i + 1]->EventTime) + 60); + } else { + $datetime = $history->EventDate . " " . $history->EventTime; + } + $info->appendHistoryEntry(new TrackingEntry( + TrackingStatus::USPSEventCodeToStatus((string) $history->EventCode), + ((string) $history->Event) . (TrackingStatus::USPSEventCodeToStatus((string) $history->EventCode) == TrackingStatus::TRACKING_STATUS_UNKNOWN ? " " . (string) $history->EventCode : ""), + $datetime, + $location, + TrackingStatus::isUSPSEventCodeContainerScan((string) $history->EventCode))); + } + + return $info; + } + +} diff --git a/lib/USPSAPIs.lib.php b/lib/USPSAPIs.lib.php new file mode 100644 index 0000000..3987f21 --- /dev/null +++ b/lib/USPSAPIs.lib.php @@ -0,0 +1,51 @@ +get("logistics.tracking.usps_bearer_token") != false) { + return $memcache->get("logistics.tracking.usps_bearer_token"); + } + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, 'https://api.usps.com/oauth2/v3/token'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST'); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + ]); + curl_setopt($ch, CURLOPT_POSTFIELDS, "{\"client_id\": \"$clientid\", \"client_secret\": \"$clientsecret\", \"grant_type\": \"client_credentials\", \"scope\": \"addresses international-prices pickup tracking scan-forms locations prices\"}"); + $response = curl_exec($ch); + curl_close($ch); + $data = json_decode($response, true); + $memcache->set("logistics.tracking.usps_bearer_token", $data["access_token"], ($data["expires_in"] * 1) - 120); + return $data["access_token"]; + } + + public static function getAPIRequest($endpoint) { + $headers = [ + "Authorization: Bearer " . USPSAPIs::getBearerToken(true), + 'Content-Type: application/json' + ]; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($ch, CURLOPT_TIMEOUT, 45); + curl_setopt($ch, CURLOPT_URL, "https://api.usps.com/" . $endpoint); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_ENCODING, ""); + $response = curl_exec($ch); + + return $response; + } +}