BROOKO icon
BROOKO UK NETWORK
Where code meets creativity & adventure
File viewer

trutrak_soap.php

Type
php
Size
16.78 KB
Modified
15 May
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,
    ];
}