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;
+ }
+}