Files
api/services/nginx/app/classes/motorapi.php
T

414 lines
14 KiB
PHP

<?php
namespace classes;
require_once WD . '/modules/motorapi/motorapi_c.php';
require_once WD . '/modules/motorapi/helpers/motorapi_vehicle_types.php';
/** Actions */
require_once WD . '/modules/motorapi/actions/license_plate_lookup_a.php';
use Exception;
use interfaces\motorapi_i;
use motorapi\actions\license_plate_lookup_a;
use motorapi\helpers\motorapi_vehicle_types;
use motorapi\motorapi_c;
use objects\motorapi_lookups_o;
use Throwable;
class motorapi implements motorapi_i
{
/**
* Configuration of the motorapi module
* @var motorapi_c
*/
public motorapi_c $config;
/**
* Vehicle types
* @var motorapi_vehicle_types
*/
public motorapi_vehicle_types $vehicle_types;
/**
* API URL
* @var string
*/
private string $api_url = 'https://v1.motorapi.dk/';
/**
* ACTION: LICENSE_PLATE_LOOKUP
* @see license_plate_lookup_a
* @notation This action is when a license plate lookup request is made, and logs the request in the database
* @var license_plate_lookup_a
*/
private license_plate_lookup_a $license_plate_lookup;
public function __construct()
{
$this->config = new motorapi_c();
$this->vehicle_types = new motorapi_vehicle_types();
/** Actions */
$this->license_plate_lookup = new license_plate_lookup_a();
}
private static function cleanJSON(mixed $value): string
{
if (is_string($value)) {
// Remove backslashes
$value = stripslashes($value);
$value = stripslashes($value);
// Ensure the string is valid UTF-8
$value = mb_convert_encoding($value, 'UTF-8', 'UTF-8');
// Optionally, you can use regex to remove any non-printable characters
$value = preg_replace('/[^\x20-\x7E]/', '', $value);
} else {
echo 'Unable to perform cleaning on value of type ' . gettype($value) . ': ' . print_r($value, true) . PHP_EOL;
}
// Remove any invalid UTF-8 characters
return $value;
}
/**
* Get the recommended products for a license plate
* @param string $plate The license plate
* @return array The recommended products
* @throws Exception If the module is not enabled
* @throws Exception If the license plate is invalid
* @throws Exception If the daily limit is exceeded
* @throws Exception If the secret key is invalid
*/
public function getRecommendedProducts(string $plate): array
{
self::requireModuleEnabled();
self::requireValidLicensePlate($plate);
$data = self::getLicensePlateInformation($plate, false);
// Get the recommended products for the license plate
return match ($data->type) {
// TODO: Add recommended products for each vehicle type, and convert this into an object type.
$this->vehicle_types->PLATE_TYPE_CAR => [
1,
2,
],
$this->vehicle_types->PLATE_TYPE_TRUCK => [
3,
4,
],
default => [],
};
}
/**
* @inheritDoc
*/
function requireModuleEnabled(): void
{
// Check if the module is enabled
if (!$this->config->enabled->isTrue()) {
throw new Exception('The motorapi module is not enabled');
}
}
/**
* @inheritDoc
*/
function requireValidLicensePlate(string $licensePlate): void
{
// Check if the license plate is valid
if (!preg_match('/^[A-Z0-9]{1,10}$/', $licensePlate)) {
throw new Exception('Invalid license plate');
}
}
/**
* @inheritDoc
* @throws Exception If the module is not enabled, the license plate is invalid, the daily limit is exceeded, or the secret key is invalid
*/
function getLicensePlateInformation(string $licensePlate, bool $ignoreCache = true): object
{
// Check if the license plate information is cached
if (!$ignoreCache && self::isCached($licensePlate)) {
// Get the cached result
$tmp_res = self::getCachedResult($licensePlate);
if ($tmp_res !== null) {
return $tmp_res;
}
}
// Get the license plate information from the motorapi
return $this->sendRequest($licensePlate, 'vehicles', [], 'GET');
}
/**
* @inheritDoc
*/
function isCached(string $licensePlate): bool
{
// Check if the license plate response is stored in the log/local database/cache
$motorapi_lookups = new motorapi_lookups_o();
return $motorapi_lookups->isCached($licensePlate);
}
/**
* @inheritDoc
*/
function getCachedResult(string $licensePlate): ?object
{
global /** @var response $response */
$response;
// Get the cached result from the log/local database/cache
$motorapi_lookups = new motorapi_lookups_o();
// Add the cached value to the meta
self::addCachedMetaIfPossible($response);
$cleaned_result = self::cleanJSON($motorapi_lookups->getCachedResult($licensePlate)->result->value());
$object = json_decode($cleaned_result);
if ($object === null) {
print_r($cleaned_result);
throw new Exception('Cached result is not valid JSON');
}
return json_decode($cleaned_result);
}
public static function addCachedMetaIfPossible(mixed $response): void
{
if (is_object($response) && method_exists($response, 'add_meta')) {
$response->add_meta('cached', true);
}
}
/**
* @inheritDoc
* @throws Exception If the module is not enabled, the license plate is invalid, the daily limit is exceeded, or the secret key is invalid
*/
function sendRequest(string $licensePlate, string $endpoint, array $data = [], string $method = 'GET'): object
{
// Validate the module is enabled
self::requireModuleEnabled();
// Validate the license plate
self::requireValidLicensePlate($licensePlate);
// Validate the secret key
self::requireValidSecretKey();
// Reserve quota for the outbound provider call. Cache hits return before this point.
$this->reserveLookupQuota($licensePlate, $endpoint, $method);
// Send the request
$response = match ($method) {
'GET' => self::sendGetRequest($licensePlate, $endpoint, $data),
'POST' => self::sendPostRequest($licensePlate, $endpoint, $data),
'PUT' => self::sendPutRequest($licensePlate, $endpoint, $data),
'DELETE' => self::sendDeleteRequest($licensePlate, $endpoint, $data),
default => self::exception(
$licensePlate, [
'method' => $method,
'endpoint' => $endpoint,
'data' => $data,
'license_plate' => $licensePlate,
'response' => null,
'status_code' => 400,
'error' => 'Invalid request method',
],
500
),
};
// Add the request to the log
self::addRequestToLog($licensePlate, $endpoint, $response);
// Return the response
return $response;
}
/**
* @inheritDoc
*/
function requireDailyLimitNotExceeded(): void
{
// Check if the daily limit is exceeded
if ($this->getDailyRequestCounter() >= (int)$this->config->daily_limit->getVariableValue()) {
throw new Exception('Daily limit exceeded');
}
}
/**
* @inheritDoc
*/
function getDailyRequestCounter(): int
{
try {
return (new module_usage_service())->currentUsedQuantity('motorapi', 'lookup_calls');
} catch (Throwable) {
}
// Count the rows from the motorapi request log that was made today
$motorapi_lookups = new motorapi_lookups_o();
$motorapi_lookups->getTodayCount();
return $motorapi_lookups->getTodayCount();
}
/**
* @throws Exception
*/
private function reserveLookupQuota(string $licensePlate, string $endpoint, string $method): void
{
(new module_usage_service())->reserveOrFail('motorapi', 'lookup_calls', 1, [
'license_plate' => $licensePlate,
'endpoint' => $endpoint,
'method' => strtoupper($method),
]);
}
/**
* @inheritDoc
*/
function requireValidSecretKey(): void
{
// Check if the secret key is valid
if ($this->config->secret_key->getVariableValue() === null) {
self::exception(
'',
[
'status_code' => 500,
'error' => 'Invalid secret key defined in the config (motorapi_secret_key_c)',
],
500
);
}
}
/**
* @throws Exception
*/
function exception(string $licensePlate, array $data = [], int $status_code = 500): exception
{
// Add the request to the log
$this->license_plate_lookup->license_plate_lookup($licensePlate, $data, $status_code);
return throw new Exception($data['error'] ?? 'An error occurred while processing the request, in ' . $this->config->getModuleName() . ' module');
}
/**
* @inheritDoc
* @throws Exception
*/
function sendGetRequest(string $licensePlate, string $endpoint, array $data): object
{
// Build the query string if any data is provided
$query_string = '';
if (!empty($data)) {
$query_string = '?' . http_build_query($data);
}
// Initialize cURL session
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint . '/' . $licensePlate . $query_string);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Set HTTP headers including the X-AUTH-TOKEN
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'X-AUTH-TOKEN: ' . $this->config->secret_key->getVariableValue(),
]);
// Execute the request and handle response
$output = curl_exec($ch);
// Check for errors
if (curl_errno($ch)) {
self::exception(
$licensePlate, [
'method' => 'GET',
'endpoint' => $endpoint,
'data' => $data,
'license_plate' => $licensePlate,
'response' => null,
'status_code' => 500,
'error' => curl_error($ch),
],
500
);
}
// Get the status code
$status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Check if the response is valid JSON
if (!json_decode($output)) {
$exception_message = match ($status_code) {
401 => 'Unauthorized',
404 => 'Not found',
429 => 'Too many requests',
default => 'Invalid response',
};
self::exception(
$licensePlate, [
'method' => 'GET',
'endpoint' => $endpoint,
'data' => $data,
'license_plate' => $licensePlate,
'response' => null,
'status_code' => $status_code,
'error' => $exception_message,
],
$status_code
);
}
$this->license_plate_lookup->license_plate_lookup($licensePlate, json_decode($output), $status_code);
return json_decode($output);
}
/**
* @inheritDoc
*/
function sendPostRequest(string $licensePlate, string $endpoint, array $data): object
{
// Send a POST request to the motorapi
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
$this->license_plate_lookup->license_plate_lookup($licensePlate, json_decode($output), 200);
return json_decode($output);
}
/**
* @inheritDoc
*/
function sendPutRequest(string $licensePlate, string $endpoint, array $data): object
{
// Send a PUT request to the motorapi
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
$this->license_plate_lookup->license_plate_lookup($licensePlate, json_decode($output), 200);
return json_decode($output);
}
/**
* @inheritDoc
*/
function sendDeleteRequest(string $licensePlate, string $endpoint, array $data): object
{
// Send a DELETE request to the motorapi
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
$this->license_plate_lookup->license_plate_lookup($licensePlate, json_decode($output), 200);
return json_decode($output);
}
/**
* @inheritDoc
* @throws Exception
*/
function addRequestToLog(string $licensePlate, string $endpoint, object $response): void
{
// Add the request to the motorapi request log
$motorapi_lookups = new motorapi_lookups_o();
$motorapi_lookups->add($licensePlate, json_encode($response), $endpoint);
}
}