trutrak_soap.php
16.78 KB
<?php
/**
* TruTrak SOAP (WSDL) client for WorkersPanel
*
* Base example:
* Base URL: https://ttapi.trutrakpro.co.uk
* WSDL: https://ttapi.trutrakpro.co.uk/?WSDL
*
* We avoid SoapClient because it's often disabled on shared hosting.
* Instead we post SOAP XML via cURL and parse the returned dataset.
*/
// This file expects bootstrap.php to already be loaded (PDO + helper functions)
function tt_wsdl_url(string $baseUrl): string {
$baseUrl = trim($baseUrl);
if ($baseUrl === '') return '';
if (!preg_match('#^https?://#i', $baseUrl)) $baseUrl = 'https://' . $baseUrl;
$baseUrl = rtrim($baseUrl, '/');
return $baseUrl . '/?WSDL';
}
function tt_sys_get(string $key, string $default = ''): string {
return getSystemInfo($key, $default);
}
function tt_sys_set(string $key, string $value): void {
global $pdo;
$stmt = $pdo->prepare("INSERT INTO system_info (`key`,`value`) VALUES (?,?) ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)");
$stmt->execute([$key, $value]);
}
function tt_get_cfg(): array {
$cfg = [
'base_url' => tt_sys_get('trutrak_base_url', 'https://ttapi.trutrakpro.co.uk'),
'login' => tt_sys_get('trutrak_login', ''),
'password' => tt_sys_get('trutrak_password', ''),
'skey' => tt_sys_get('trutrak_secret_key', ''),
'auth_mode' => tt_sys_get('trutrak_auth_mode', 'U'), // C customer, U user
'xmltype' => (int)tt_sys_get('trutrak_xmltype', '0'),
'token' => tt_sys_get('trutrak_token', ''),
'token_exp' => (int)tt_sys_get('trutrak_token_expires', '0'),
'daily_ts' => (int)tt_sys_get('trutrak_dailytrail_last_ts', '0'),
];
$b = trim($cfg['base_url']);
if ($b && !preg_match('#^https?://#i', $b)) $b = 'https://' . $b;
$cfg['base_url'] = rtrim($b, '/');
$cfg['auth_mode'] = strtoupper(trim((string)$cfg['auth_mode']));
if (!in_array($cfg['auth_mode'], ['C', 'U'], true)) $cfg['auth_mode'] = 'C';
$cfg['xmltype'] = in_array($cfg['xmltype'], [0, 1, 2], true) ? $cfg['xmltype'] : 0;
return $cfg;
}
function tt_http_get(string $url, int $timeout = 20): array {
if (!function_exists('curl_init')) return ['ok'=>false,'code'=>0,'raw'=>'','error'=>'cURL is not available (curl_init missing)'];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_USERAGENT, 'WorkersPanel/' . (defined('APP_VERSION') ? APP_VERSION : 'unknown'));
$raw = curl_exec($ch);
$err = curl_error($ch);
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($raw === false) return ['ok' => false, 'code' => 0, 'raw' => '', 'error' => $err ?: 'Request failed'];
return ['ok' => ($code >= 200 && $code < 300), 'code' => $code, 'raw' => (string)$raw, 'error' => ($code >= 200 && $code < 300) ? null : ('HTTP ' . $code)];
}
function tt_wsdl_cache_path(string $baseUrl): string {
return rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'workerspanel_trutrak_wsdl_' . md5($baseUrl) . '.xml';
}
function tt_load_wsdl(string $baseUrl, bool $forceRefresh = false): array {
$wsdlUrl = tt_wsdl_url($baseUrl);
if ($wsdlUrl === '') return ['ok' => false, 'error' => 'Missing base URL'];
$cache = tt_wsdl_cache_path($baseUrl);
if (!$forceRefresh && is_file($cache) && (time() - filemtime($cache)) < 3600) {
$raw = @file_get_contents($cache);
if (is_string($raw) && trim($raw) !== '') return ['ok' => true, 'raw' => $raw, 'url' => $wsdlUrl, 'cached' => true];
}
$r = tt_http_get($wsdlUrl, 25);
if (!$r['ok']) {
$alt = rtrim($baseUrl, '/') . '?WSDL';
$r = tt_http_get($alt, 25);
if (!$r['ok']) return ['ok' => false, 'error' => $r['error'] ?: 'Failed to load WSDL', 'url' => $wsdlUrl];
$wsdlUrl = $alt;
}
@file_put_contents($cache, $r['raw']);
return ['ok' => true, 'raw' => $r['raw'], 'url' => $wsdlUrl, 'cached' => false];
}
function tt_wsdl_info(string $wsdlXml): array {
$out = [
'targetNamespace' => 'http://www.trutrakpro.co.uk/',
'endpoint' => null,
];
$dom = new DOMDocument();
if (!@$dom->loadXML($wsdlXml)) return $out;
$xp = new DOMXPath($dom);
$defs = $xp->query("//*[local-name()='definitions']");
if ($defs && $defs->length) {
$tns = $defs->item(0)->attributes?->getNamedItem('targetNamespace')?->nodeValue;
if (is_string($tns) && $tns !== '') $out['targetNamespace'] = $tns;
}
$addr = $xp->query("//*[local-name()='address' and (@location)]");
if ($addr && $addr->length) {
$loc = $addr->item(0)->attributes?->getNamedItem('location')?->nodeValue;
if (is_string($loc) && $loc !== '') $out['endpoint'] = $loc;
}
return $out;
}
function tt_wsdl_has_method(string $wsdlXml, string $method): bool {
if (!class_exists('DOMDocument')) return false;
$dom = new DOMDocument();
if (!@$dom->loadXML($wsdlXml)) return false;
$xp = new DOMXPath($dom);
$q = "//*[local-name()='element' and @name='" . htmlspecialchars($method, ENT_QUOTES) . "']";
$n = $xp->query($q);
return $n && $n->length > 0;
}
function tt_wsdl_method_params(string $wsdlXml, string $method): array {
if (!class_exists('DOMDocument')) return [];
$dom = new DOMDocument();
if (!@$dom->loadXML($wsdlXml)) return [];
$xp = new DOMXPath($dom);
$q = "//*[local-name()='element' and @name='" . htmlspecialchars($method, ENT_QUOTES) . "']//*[local-name()='sequence']/*[local-name()='element']/@name";
$attrs = $xp->query($q);
$out = [];
if ($attrs) {
foreach ($attrs as $a) {
$name = (string)$a->nodeValue;
if ($name !== '') $out[] = $name;
}
}
return $out;
}
function tt_xml_escape(string $s): string {
return htmlspecialchars($s, ENT_XML1 | ENT_QUOTES, 'UTF-8');
}
function tt_soap_post(string $endpoint, string $soapAction, string $xmlBody): array {
$ch = curl_init($endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xmlBody);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: text/xml; charset=utf-8',
'SOAPAction: "' . $soapAction . '"',
'Accept: text/xml',
]);
curl_setopt($ch, CURLOPT_USERAGENT, 'WorkersPanel/' . (defined('APP_VERSION') ? APP_VERSION : 'unknown'));
$raw = curl_exec($ch);
$err = curl_error($ch);
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($raw === false) return ['ok' => false, 'code' => 0, 'raw' => '', 'error' => $err ?: 'SOAP request failed'];
return ['ok' => ($code >= 200 && $code < 300), 'code' => $code, 'raw' => (string)$raw, 'error' => ($code >= 200 && $code < 300) ? null : ('HTTP ' . $code)];
}
function tt_parse_soap_result(string $soapXml, string $method): array {
if (!class_exists('DOMDocument')) return ['ok'=>false,'error'=>'DOMDocument missing'];
$dom = new DOMDocument();
if (!@$dom->loadXML($soapXml)) return ['ok' => false, 'error' => 'Invalid SOAP XML'];
$xp = new DOMXPath($dom);
$n = $xp->query("//*[local-name()='" . $method . "Result']");
if (!$n || !$n->length) {
$fault = $xp->query("//*[local-name()='Fault']");
if ($fault && $fault->length) return ['ok' => false, 'error' => trim($fault->item(0)->textContent) ?: 'SOAP Fault'];
return ['ok' => false, 'error' => 'No result found'];
}
$node = $n->item(0);
$text = trim($node->textContent ?? '');
$inner = '';
foreach ($node->childNodes as $child) {
if ($child->nodeType === XML_ELEMENT_NODE) $inner .= $dom->saveXML($child);
}
return ['ok' => true, 'text' => $text, 'inner' => $inner];
}
function tt_dataset_rows(string $xmlMaybe): array {
$xmlMaybe = trim($xmlMaybe);
if ($xmlMaybe === '') return [];
$dom = new DOMDocument();
if (!@$dom->loadXML($xmlMaybe)) return [];
$xp = new DOMXPath($dom);
$nds = $xp->query("//*[local-name()='NewDataSet']");
if ($nds && $nds->length) {
$rows = [];
$rowNodes = $xp->query("//*[local-name()='NewDataSet']/*");
foreach ($rowNodes as $rn) {
if ($rn->nodeType !== XML_ELEMENT_NODE) continue;
$row = [];
foreach ($rn->childNodes as $c) {
if ($c->nodeType !== XML_ELEMENT_NODE) continue;
$row[$c->localName] = trim($c->textContent);
}
if (!empty($row)) $rows[] = $row;
}
return $rows;
}
// fallback: root children
$root = $dom->documentElement;
if (!$root) return [];
$rows = [];
foreach ($root->childNodes as $rn) {
if ($rn->nodeType !== XML_ELEMENT_NODE) continue;
$row = [];
foreach ($rn->childNodes as $c) {
if ($c->nodeType !== XML_ELEMENT_NODE) continue;
$row[$c->localName] = trim($c->textContent);
}
if (!empty($row)) $rows[] = $row;
}
return $rows;
}
function tt_build_params(string $wsdlXml, array $cfg, string $method, array $inputs = []): array {
$want = tt_wsdl_method_params($wsdlXml, $method);
$params = [];
foreach ($want as $p) {
$pl = strtolower($p);
if ($pl === 'token') { $params[$p] = (string)($inputs['token'] ?? ''); continue; }
if ($pl === 'login') { $params[$p] = (string)$cfg['login']; continue; }
if ($pl === 'password') { $params[$p] = (string)$cfg['password']; continue; }
if (in_array($pl, ['skey','secretkey','secret_key'], true)) { $params[$p] = (string)$cfg['skey']; continue; }
if (str_contains($pl, 'auth') && str_contains($pl, 'mode')) { $params[$p] = (string)$cfg['auth_mode']; continue; }
if ($pl === 'xmltype') { $params[$p] = (string)$cfg['xmltype']; continue; }
if (str_contains($pl, 'filter_assets')) { $params[$p] = (string)($inputs['filter_assets'] ?? ''); continue; }
if (str_contains($pl, 'filter_registration')) { $params[$p] = (string)($inputs['filter_registration'] ?? ''); continue; }
if (str_contains($pl, 'filter_depots')) { $params[$p] = (string)($inputs['filter_depots'] ?? ''); continue; }
if (str_contains($pl, 'filter_depot')) { $params[$p] = (string)($inputs['filter_depot'] ?? ''); continue; }
if (str_contains($pl, 'filter_waypoints')) { $params[$p] = (string)($inputs['filter_waypoints'] ?? ''); continue; }
if (str_contains($pl, 'after') && (str_contains($pl, 'record') || str_contains($pl, 'id'))) {
$params[$p] = (string)($inputs['after_record'] ?? '0');
continue;
}
if (str_contains($pl, 'date')) {
if (!empty($inputs['date']) && ($pl === 'date' || str_contains($pl, 'day'))) { $params[$p] = (string)$inputs['date']; continue; }
if (!empty($inputs['start']) && (str_contains($pl, 'start') || str_contains($pl, 'from') || str_contains($pl, 'date1') || str_contains($pl, 'begin'))) { $params[$p] = (string)$inputs['start']; continue; }
if (!empty($inputs['end']) && (str_contains($pl, 'end') || str_contains($pl, 'to') || str_contains($pl, 'date2') || str_contains($pl, 'finish'))) { $params[$p] = (string)$inputs['end']; continue; }
}
if (array_key_exists($p, $inputs)) $params[$p] = (string)$inputs[$p];
}
return $params;
}
function tt_call(array $cfg, string $method, array $inputs = [], bool $forceWsdlRefresh = false): array {
if (empty($cfg['base_url'])) return ['ok' => false, 'error' => 'Missing base URL'];
$wsdl = tt_load_wsdl($cfg['base_url'], $forceWsdlRefresh);
if (!$wsdl['ok']) return ['ok' => false, 'error' => $wsdl['error'] ?? 'Failed to load WSDL'];
$info = tt_wsdl_info($wsdl['raw']);
$tns = $info['targetNamespace'] ?: 'http://www.trutrakpro.co.uk/';
$endpoint = $info['endpoint'] ?: ($cfg['base_url'] . '/');
$params = tt_build_params($wsdl['raw'], $cfg, $method, $inputs);
$body = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
$body .= "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">";
$body .= "<soap:Body>";
$body .= "<" . $method . " xmlns=\"" . tt_xml_escape($tns) . "\">";
foreach ($params as $k => $v) {
$body .= "<" . $k . ">" . tt_xml_escape((string)$v) . "</" . $k . ">";
}
$body .= "</" . $method . ">";
$body .= "</soap:Body></soap:Envelope>";
$soapAction = rtrim($tns, '/') . '/' . $method;
$resp = tt_soap_post($endpoint, $soapAction, $body);
if (!$resp['ok']) return ['ok' => false, 'error' => $resp['error'] ?? 'SOAP call failed', 'http_code' => $resp['code'] ?? 0, 'endpoint' => $endpoint];
$parsed = tt_parse_soap_result($resp['raw'], $method);
if (!$parsed['ok']) return ['ok' => false, 'error' => $parsed['error'] ?? 'Failed to parse SOAP result', 'endpoint' => $endpoint];
$payload = $parsed['inner'] !== '' ? $parsed['inner'] : $parsed['text'];
$rows = tt_dataset_rows($payload);
return ['ok' => true, 'rows' => $rows, 'payload' => $payload, 'endpoint' => $endpoint, 'tns' => $tns, 'wsdl_url' => $wsdl['url'] ?? null];
}
function tt_get_token(array $cfg, bool $force = false): array {
$now = time();
if (!$force && !empty($cfg['token']) && !empty($cfg['token_exp']) && $cfg['token_exp'] > ($now + 300)) {
return ['ok' => true, 'token' => $cfg['token'], 'cached' => true, 'expires' => $cfg['token_exp']];
}
if ($cfg['login'] === '' || $cfg['password'] === '' || $cfg['skey'] === '') {
return ['ok' => false, 'error' => 'Missing TruTrak Login / Password / Secret key'];
}
$modesToTry = [];
$firstMode = $cfg['auth_mode'] ?: 'U';
$firstMode = in_array($firstMode, ['C','U'], true) ? $firstMode : 'U';
$modesToTry[] = $firstMode;
$modesToTry[] = ($firstMode === 'U') ? 'C' : 'U';
$res = null;
$usedMode = $firstMode;
foreach ($modesToTry as $mode) {
$usedMode = $mode;
$tmpCfg = $cfg;
$tmpCfg['auth_mode'] = $mode;
$res = tt_call($tmpCfg, 'TTAuthenticate', [
'Login' => $cfg['login'],
'Password' => $cfg['password'],
'SKey' => $cfg['skey'],
'Auth_Mode' => $mode,
]);
if ($res && !empty($res['ok'])) break;
}
if (!$res || empty($res['ok'])) return $res ?: ['ok'=>false,'error'=>'Authentication failed'];
// Persist the working mode so next call doesn't fail first.
if ($usedMode !== $cfg['auth_mode']) {
tt_sys_set('trutrak_auth_mode', $usedMode);
$cfg['auth_mode'] = $usedMode;
}
$token = trim(strip_tags((string)($res['payload'] ?? '')));
if ($token === '' && !empty($res['rows']) && isset($res['rows'][0])) {
$first = $res['rows'][0];
$token = trim((string)reset($first));
}
if ($token === '') return ['ok' => false, 'error' => 'Authentication succeeded but token was empty'];
$exp = $now + (23 * 3600);
tt_sys_set('trutrak_token', $token);
tt_sys_set('trutrak_token_expires', (string)$exp);
return ['ok' => true, 'token' => $token, 'cached' => false, 'expires' => $exp];
}
function tt_pick(array $row, array $keys, $default = null) {
foreach ($keys as $k) {
if (array_key_exists($k, $row) && $row[$k] !== '') return $row[$k];
foreach ($row as $rk => $rv) {
if (strcasecmp($rk, $k) === 0 && $rv !== '') return $rv;
}
}
return $default;
}
function tt_norm_location(array $r): array {
$assetId = tt_pick($r, ['Assetid','AssetId','AssetID','Asset_ID','Id','ID']);
$reg = tt_pick($r, ['Registration','Reg','Vehicle','Name','AssetRegistration']);
$lat = tt_pick($r, ['Latitude','Lat','LAT']);
$lon = tt_pick($r, ['Longitude','Lon','Lng','LON','Long']);
$speed = tt_pick($r, ['Speed','Spd','SPD']);
$addr = tt_pick($r, ['Address','Location','Street','NearestAddress']);
$time = tt_pick($r, ['DateTime','Datetime','Time','FixTime','GPSDate','LastSeen','LastPositionDate']);
$status = tt_pick($r, ['Status','State','Motion','Ignition','Live']);
$odo = tt_pick($r, ['Odometer','Odo']);
$battI = tt_pick($r, ['IntBatteryVoltage','BatteryInt','IntBatt']);
$battE = tt_pick($r, ['ExtBatteryVoltage','BatteryExt','ExtBatt']);
return [
'deviceId' => $assetId,
'name' => $reg ?: ('Asset ' . ($assetId ?? '')),
'uniqueId' => $reg ?: (string)($assetId ?? ''),
'status' => $status,
'latitude' => $lat,
'longitude' => $lon,
'speed' => $speed,
'address' => $addr,
'fixTime' => $time,
'odometer' => $odo,
'batteryInt' => $battI,
'batteryExt' => $battE,
'raw' => $r,
];
}