Initial IoT infrastructure snapshot

This commit is contained in:
XupaMisto 2026-05-11 23:31:48 +01:00
commit 269ea239f2
345 changed files with 44204 additions and 0 deletions

3
.env Normal file
View File

@ -0,0 +1,3 @@
APP_ENV=prod
APP_KEY=5e13d830372558455fee77ea751fc0d57b3ea25a5346239de690a262e73c7b16
SQLITE_PATH=/var/www/html/api/iot.sqlite

11
.well-known/matrix/client Normal file
View File

@ -0,0 +1,11 @@
{
"m.homeserver": {
"base_url": "https://matrix.xupas.mywire.org"
},
"io.element.call": {
"url": "https://voice.xupas.mywire.org"
},
"m.identity_server": {
"base_url": "https://matrix.xupas.mywire.org"
}
}

8
_hdr.php Normal file
View File

@ -0,0 +1,8 @@
<?php
header('Content-Type: application/json');
echo json_encode([
'AUTH' => $_SERVER['HTTP_AUTHORIZATION'] ?? '',
'XKEY' => $_SERVER['HTTP_X_API_KEY'] ?? '',
'IP' => $_SERVER['REMOTE_ADDR'] ?? '',
'UA' => $_SERVER['HTTP_USER_AGENT'] ?? '',
], JSON_PRETTY_PRINT);

1
api/.admin_token Normal file
View File

@ -0,0 +1 @@
2457622d7cdf4ca77076181b674d9dcfe0c02f9cb0081476fb95dfbd074f185d

1
api/.adminer_pwd Normal file
View File

@ -0,0 +1 @@
master

14
api/_ack_patch.php Normal file
View File

@ -0,0 +1,14 @@
<?php
// PATCH: handler robusto para /api/commands/ack
$j = json_decode(file_get_contents('php://input'), true) ?: [];
$id = isset($j['id']) ? (int)$j['id'] : 0;
$ok = isset($j['success']) ? (int)!!$j['success'] : (isset($j['ok']) ? (int)!!$j['ok'] : 1);
if ($id<=0) { http_response_code(400); echo json_encode(['error'=>'id required']); exit; }
$stmt = $pdo->prepare("UPDATE commands
SET status = CASE WHEN :ok=1 THEN 'done' ELSE 'failed' END
WHERE id=:id AND status!='done'");
$stmt->execute([':ok'=>$ok, ':id'=>$id]);
echo json_encode(['updated'=>$stmt->rowCount(), 'id'=>$id, 'success'=>(bool)$ok]);
exit;

8
api/_hdr.php Normal file
View File

@ -0,0 +1,8 @@
<?php
header('Content-Type: application/json');
echo json_encode([
'AUTH' => $_SERVER['HTTP_AUTHORIZATION'] ?? '',
'XKEY' => $_SERVER['HTTP_X_API_KEY'] ?? '',
'IP' => $_SERVER['REMOTE_ADDR'] ?? '',
'UA' => $_SERVER['HTTP_USER_AGENT'] ?? '',
], JSON_PRETTY_PRINT);

31
api/bootstrap.php Normal file
View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
require __DIR__ . '/../vendor/autoload.php';
use Dotenv\Dotenv;
$dotenv = Dotenv::createImmutable(__DIR__ . '/..'); $dotenv->safeLoad();
const JSON_OPTS = JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE;
function json($data, int $code=200): void {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_OPTS); exit;
}
function db(): PDO {
static $pdo; if ($pdo) return $pdo;
$path = $_ENV['SQLITE_PATH'] ?? (__DIR__.'/iot.sqlite');
$pdo = new PDO('sqlite:'.$path, null, null, [
PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC,
]);
$pdo->exec("
CREATE TABLE IF NOT EXISTS device (id TEXT PRIMARY KEY, token TEXT NOT NULL, created_at TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS reading (id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT NOT NULL, ts INTEGER NOT NULL, data TEXT NOT NULL);
CREATE INDEX IF NOT EXISTS idx_reading_dev_ts ON reading(device_id, ts);
");
return $pdo;
}
function require_token(PDO $pdo): string {
$hdr=$_SERVER['HTTP_AUTHORIZATION']??''; $api=$_SERVER['HTTP_X_API_KEY']??''; $t='';
if (preg_match('/Bearer\s+(.+)/i',$hdr,$m)) $t=trim($m[1]); elseif ($api) $t=$api;
if ($t==='') json(['error'=>'missing token'],401);
$st=$pdo->prepare("SELECT id FROM device WHERE token=:t LIMIT 1"); $st->execute([':t'=>$t]); $r=$st->fetch();
if(!$r) json(['error'=>'invalid token'],401); return $r['id'];
}

55
api/commands/ack.php Normal file
View File

@ -0,0 +1,55 @@
<?php
require __DIR__ . '/../db.php'; // ligação PDO $pdo
header('Content-Type: application/json');
// Ler JSON do POST
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['uid']) || empty($input['acks']) || !is_array($input['acks'])) {
http_response_code(400);
echo json_encode(['ok'=>false,'err'=>'dados_invalidos']);
exit;
}
$uid = $input['uid'];
$acks = $input['acks'];
try {
$pdo->beginTransaction();
// Obter ID do device
$stmt = $pdo->prepare("SELECT id FROM devices WHERE uid=? LIMIT 1 FOR UPDATE");
$stmt->execute([$uid]);
$dev = $stmt->fetchColumn();
if (!$dev) {
http_response_code(404);
echo json_encode(['ok'=>false,'err'=>'device']);
exit;
}
// Atualizar status de cada comando
$stmtDone = $pdo->prepare("UPDATE commands SET status='done', ack_ts=NOW() WHERE id=? AND device_id=?");
$stmtFailed = $pdo->prepare("UPDATE commands SET status='failed', ack_ts=NOW(), error=? WHERE id=? AND device_id=?");
foreach ($acks as $ack) {
if (!isset($ack['id'])) continue;
$cmdId = intval($ack['id']);
if (!empty($ack['ok'])) {
$stmtDone->execute([$cmdId, $dev]);
} else {
$errMsg = isset($ack['err']) ? substr($ack['err'], 0, 255) : 'erro';
$stmtFailed->execute([$errMsg, $cmdId, $dev]);
}
}
// Atualizar heartbeat do device
$pdo->prepare("UPDATE devices SET last_seen=NOW(), status='online' WHERE id=?")->execute([$dev]);
$pdo->commit();
echo json_encode(['ok'=>true]);
} catch (Exception $e) {
$pdo->rollBack();
http_response_code(500);
echo json_encode(['ok'=>false,'err'=>$e->getMessage()]);
}

36
api/commands/enqueue.php Normal file
View File

@ -0,0 +1,36 @@
<?php
require __DIR__ . '/../db.php';
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || !isset($input['uid'], $input['cmd'])) {
http_response_code(400);
echo json_encode(['ok' => false, 'err' => 'invalid_input']);
exit;
}
$uid = $input['uid'];
$cmd = $input['cmd'];
$params = isset($input['params']) ? json_encode($input['params']) : '{}';
// Confirmar se o device existe
$stmt = $pdo->prepare("SELECT id FROM devices WHERE uid=? LIMIT 1");
$stmt->execute([$uid]);
$dev = $stmt->fetchColumn();
if (!$dev) {
http_response_code(404);
echo json_encode(['ok' => false, 'err' => 'device_not_found']);
exit;
}
// Inserir comando
$stmt = $pdo->prepare("
INSERT INTO commands (device_id, cmd, params, status, queued_at)
VALUES (?, ?, ?, 'pending', NOW())
");
$stmt->execute([$dev, $cmd, $params]);
$id = $pdo->lastInsertId();
echo json_encode(['ok' => true, 'id' => (int)$id], JSON_UNESCAPED_SLASHES);

49
api/commands/poll.php Normal file
View File

@ -0,0 +1,49 @@
<?php
require __DIR__ . '/../db.php';
// Lê o JSON do POST
$input = json_decode(file_get_contents('php://input'), true);
if (!$input || empty($input['uid'])) {
http_response_code(400);
echo json_encode(['ok' => false, 'err' => 'missing uid']);
exit;
}
$uid = $input['uid'];
$limit = isset($input['limit']) ? (int)$input['limit'] : 20;
$pdo->beginTransaction();
// Bloqueia o device
$stmt = $pdo->prepare("SELECT id FROM devices WHERE uid=? LIMIT 1 FOR UPDATE");
$stmt->execute([$uid]);
$dev = $stmt->fetchColumn();
if (!$dev) {
$pdo->rollBack();
http_response_code(404);
echo json_encode(['ok' => false, 'err' => 'device']);
exit;
}
// Seleciona comandos pendentes
$stmt = $pdo->prepare("
SELECT id, cmd, params
FROM commands
WHERE device_id=? AND status='pending'
ORDER BY queued_at ASC
LIMIT $limit
FOR UPDATE
");
$stmt->execute([$dev]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if ($rows) {
$ids = implode(',', array_map('intval', array_column($rows, 'id')));
$pdo->exec("UPDATE commands SET status='sent', sent_at=NOW() WHERE id IN ($ids)");
$pdo->prepare("UPDATE devices SET status='online', last_seen=NOW() WHERE id=?")->execute([$dev]);
}
$pdo->commit();
header('Content-Type: application/json');
echo json_encode(['ok' => true, 'commands' => $rows], JSON_UNESCAPED_SLASHES);

11
api/composer.json Normal file
View File

@ -0,0 +1,11 @@
{
"name": "root/api",
"authors": [
{
"name": "xupa"
}
],
"require": {
"php-mqtt/client": "^2.0"
}
}

189
api/composer.lock generated Normal file
View File

@ -0,0 +1,189 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a8a3708221e95cf96b5a9d64f7dd7a56",
"packages": [
{
"name": "myclabs/php-enum",
"version": "1.8.5",
"source": {
"type": "git",
"url": "https://github.com/myclabs/php-enum.git",
"reference": "e7be26966b7398204a234f8673fdad5ac6277802"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/php-enum/zipball/e7be26966b7398204a234f8673fdad5ac6277802",
"reference": "e7be26966b7398204a234f8673fdad5ac6277802",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.3 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "1.*",
"vimeo/psalm": "^4.6.2 || ^5.2"
},
"type": "library",
"autoload": {
"psr-4": {
"MyCLabs\\Enum\\": "src/"
},
"classmap": [
"stubs/Stringable.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP Enum contributors",
"homepage": "https://github.com/myclabs/php-enum/graphs/contributors"
}
],
"description": "PHP Enum implementation",
"homepage": "https://github.com/myclabs/php-enum",
"keywords": [
"enum"
],
"support": {
"issues": "https://github.com/myclabs/php-enum/issues",
"source": "https://github.com/myclabs/php-enum/tree/1.8.5"
},
"funding": [
{
"url": "https://github.com/mnapoli",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum",
"type": "tidelift"
}
],
"time": "2025-01-14T11:49:03+00:00"
},
{
"name": "php-mqtt/client",
"version": "v2.2.0",
"source": {
"type": "git",
"url": "https://github.com/php-mqtt/client.git",
"reference": "8042ad93e72da8666e27168dc90670e45bdea274"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-mqtt/client/zipball/8042ad93e72da8666e27168dc90670e45bdea274",
"reference": "8042ad93e72da8666e27168dc90670e45bdea274",
"shasum": ""
},
"require": {
"myclabs/php-enum": "^1.7",
"php": "^8.0",
"psr/log": "^1.1|^2.0|^3.0"
},
"require-dev": {
"phpunit/php-invoker": "^3.0",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.5"
},
"suggest": {
"ext-redis": "Required for the RedisRepository"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpMqtt\\Client\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marvin Mall",
"email": "marvin-mall@msn.com",
"role": "developer"
}
],
"description": "An MQTT client written in and for PHP.",
"keywords": [
"client",
"mqtt",
"publish",
"subscribe"
],
"support": {
"issues": "https://github.com/php-mqtt/client/issues",
"source": "https://github.com/php-mqtt/client/tree/v2.2.0"
},
"time": "2024-11-24T20:54:32+00:00"
},
{
"name": "psr/log",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.2"
},
"time": "2024-09-11T13:17:53+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

34
api/contadores_json.php Normal file
View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
require __DIR__ . "/db.php";
$rows = $pdo->query("
SELECT uid, entradas, saidas, online, blocked,
TIMESTAMPDIFF(SECOND, last_seen, NOW()) AS ago
FROM contadores
ORDER BY online DESC, uid ASC
")->fetchAll();
// Totais
$sumEntradas = 0;
$sumSaidas = 0;
$onlineCount = 0;
$offlineCount = 0;
foreach ($rows as $r) {
$sumEntradas += (int)$r['entradas'];
$sumSaidas += (int)$r['saidas'];
if ($r['online'] == 1) $onlineCount++;
else $offlineCount++;
}
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: 0");
echo json_encode([
"rows" => $rows,
"online" => $onlineCount,
"offline" => $offlineCount,
"entradas" => $sumEntradas,
"saidas" => $sumSaidas,
]);

10
api/db.php Normal file
View File

@ -0,0 +1,10 @@
<?php
$pdo = new PDO(
"mysql:host=localhost;dbname=cagalhao;charset=utf8mb4",
"master",
"master",
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);

17
api/delete_device.php Normal file
View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
require __DIR__ . '/db.php';
$data = json_decode(file_get_contents("php://input"), true);
if (!$data || !isset($data['uid'])) {
http_response_code(400);
echo json_encode(["error"=>"invalid"]);
exit;
}
$uid = $data['uid'];
$stmt = $db->prepare("DELETE FROM contadores WHERE uid=?");
$stmt->execute([$uid]);
echo json_encode(["ok"=>1]);

127
api/gpio_control.php Normal file
View File

@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use PhpMqtt\Client\MqttClient;
use PhpMqtt\Client\ConnectionSettings;
// Configuração da BD
$dbHost="localhost"; $dbName="cagalhao"; $dbUser="master"; $dbPass="master";
try {
$db = new PDO("mysql:host=$dbHost;dbname=$dbName;charset=utf8",$dbUser,$dbPass);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(Throwable $e) {
die("Erro na BD: " . $e->getMessage());
}
// Configuração do MQTT
$mqttHost='xupas.mooo.com'; $mqttPort=1883; $mqttUser='xupa'; $mqttPass='xupa';
// Função para enviar comando
function sendMqttCommand(string $espId, int $pin, int $value): bool {
global $mqttHost, $mqttPort, $mqttUser, $mqttPass;
try {
$clientId = "gpio-panel-".uniqid();
$settings = (new ConnectionSettings())
->setUsername($mqttUser)
->setPassword($mqttPass)
->setKeepAliveInterval(10)
->setConnectTimeout(3)
->setSocketTimeout(3);
$mqtt = new MqttClient($mqttHost, $mqttPort, $clientId);
$mqtt->connect($settings, true);
$payload = json_encode([
'cmd' => 'GPIO',
'pin' => $pin,
'value' => $value
]);
$topic = "esp/{$espId}/cmd";
$mqtt->publish($topic, $payload, 0, false);
$mqtt->disconnect();
return true;
} catch (Throwable $e) {
error_log("MQTT-ERR) " . $e->getMessage());
return false;
}
}
// Processar pedidos
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$esp = $_POST['esp'] ?? '';
$pin = intval($_POST['pin'] ?? -1);
$val = intval($_POST['value'] ?? -1);
if ($esp && $pin >= 0 && ($val === 0 || $val === 1)) {
$ok = sendMqttCommand($esp, $pin, $val);
if ($ok) {
$msg = "✅ Comando enviado: {$esp} → Pino {$pin}".($val ? "ON" : "OFF");
} else {
$msg = "❌ Falha ao enviar comando para {$esp}";
}
} else {
$msg = "⚠️ Parâmetros inválidos";
}
}
// Buscar ESPs registados
$stmt = $db->query("SELECT uid, online, last_seen FROM contadores ORDER BY uid ASC");
$esps = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
<title>Controle GPIO via MQTT</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
</head>
<body class="bg-dark text-light">
<div class="container py-4">
<h1 class="mb-4"> Controle de GPIO (MQTT)</h1>
<?php if (!empty($msg)): ?>
<div class="alert alert-info"><?= htmlspecialchars($msg) ?></div>
<?php endif; ?>
<table class="table table-dark table-striped table-bordered align-middle">
<thead>
<tr>
<th>ESP</th>
<th>Status</th>
<th>Último Sinal</th>
<th>Pino</th>
<th>Ação</th>
</tr>
</thead>
<tbody>
<?php foreach ($esps as $esp): ?>
<tr>
<td><?= htmlspecialchars($esp['uid']) ?></td>
<td>
<?php if ($esp['online']): ?>
<span class="badge bg-success">Online</span>
<?php else: ?>
<span class="badge bg-danger">Offline</span>
<?php endif; ?>
</td>
<td><?= htmlspecialchars($esp['last_seen']) ?></td>
<td>
<form method="post" class="d-flex gap-2">
<input type="hidden" name="esp" value="<?= htmlspecialchars($esp['uid']) ?>">
<input type="number" name="pin" min="0" max="16" class="form-control form-control-sm" required>
</td>
<td>
<button type="submit" name="value" value="1" class="btn btn-success btn-sm">ON</button>
<button type="submit" name="value" value="0" class="btn btn-danger btn-sm">OFF</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</body>
</html>

202
api/index.php Normal file
View File

@ -0,0 +1,202 @@
<?php declare(strict_types=1);
require __DIR__.'/bootstrap.php';
$pdo = db();
function json_out($data, int $code=200): never {
http_response_code($code);
header('Content-Type: application/json');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/';
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$ADMIN = @trim(@file_get_contents(__DIR__.'/.admin_token')) ?: null;
/** Autentica device por token (Authorization: Bearer ... ou X-API-Key) */
function require_token_dev(PDO $pdo): string {
$hdr = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if ($hdr && stripos($hdr,'Bearer ')===0) $hdr = trim(substr($hdr,7));
if ($hdr==='') $hdr = $_SERVER['HTTP_X_API_KEY'] ?? '';
if ($hdr==='') json_out(['error'=>'auth required'], 401);
$st = $pdo->prepare("SELECT id FROM device WHERE token=:t");
$st->execute([':t'=>$hdr]);
$id = $st->fetchColumn();
if (!$id) json_out(['error'=>'invalid token'], 401);
return (string)$id;
}
/** Autentica admin por X-Admin-Token */
function require_admin_token(?string $admin): void {
if (!$admin) json_out(['error'=>'admin token missing'], 500);
$hdr = $_SERVER['HTTP_X_ADMIN_TOKEN'] ?? '';
if ($hdr !== $admin) json_out(['error'=>'admin auth'], 401);
}
/* ---------- HEALTH ---------- */
if ($path === '/api/health') json_out(['ok'=>true, 'php'=>PHP_VERSION]);
/* ---------- INGEST ---------- */
if ($path === '/api/ingest' && $method === 'POST') {
$dev = require_token_dev($pdo);
$raw = file_get_contents('php://input') ?: '';
$j = json_decode($raw, true);
if (!is_array($j)) json_out(['error'=>'invalid json'], 400);
$ts = isset($j['ts']) && is_numeric($j['ts']) ? (int)$j['ts'] : time();
$payload = $j; unset($payload['ts']);
// retry leve para "database is locked"
$tries = 0;
while (true) {
try {
$st = $pdo->prepare("INSERT INTO reading(device_id,ts,data) VALUES(:d,:t,:j)");
$st->execute([':d'=>$dev, ':t'=>$ts, ':j'=>json_encode($payload, JSON_UNESCAPED_UNICODE)]);
json_out(['stored'=>true, 'id'=>$pdo->lastInsertId()]);
} catch (PDOException $e) {
if (stripos($e->getMessage(), 'locked') !== false && ++$tries <= 5) { usleep(200000); continue; }
throw $e;
}
}
}
/* ---------- LAST ---------- */
if ($path === '/api/last' && $method === 'GET') {
$dev = $_GET['device'] ?? null; if (!$dev) json_out(['error'=>'device required'], 400);
$st=$pdo->prepare("SELECT ts,data FROM reading WHERE device_id=:d ORDER BY ts DESC LIMIT 1");
$st->execute([':d'=>$dev]); $r=$st->fetch();
if (!$r) json_out(['device'=>$dev,'reading'=>null]);
$r['data'] = json_decode($r['data'], true);
json_out(['device'=>$dev,'reading'=>$r]);
}
/* ---------- DATA ---------- */
if ($path === '/api/data' && $method === 'GET') {
$dev = $_GET['device'] ?? null;
$from= isset($_GET['from']) ? (int)$_GET['from'] : time()-86400;
$to = isset($_GET['to']) ? (int)$_GET['to'] : time();
if (!$dev) json_out(['error'=>'device required'], 400);
$st=$pdo->prepare("SELECT ts,data FROM reading WHERE device_id=:d AND ts BETWEEN :f AND :t ORDER BY ts ASC");
$st->execute([':d'=>$dev, ':f'=>$from, ':t'=>$to]);
$rows=$st->fetchAll();
foreach ($rows as &$r) $r['data']=json_decode($r['data'], true);
json_out(['device'=>$dev,'from'=>$from,'to'=>$to,'rows'=>$rows]);
}
/* ---------- STATE: SET ---------- */
if ($path === '/api/state/set' && $method === 'POST') {
require_admin_token($ADMIN);
$raw=file_get_contents('php://input') ?: '';
$j = json_decode($raw, true);
if (!is_array($j) || empty($j['device'])) json_out(['error'=>'device required'], 400);
$dev = $j['device']; $now = time();
$pairs=[];
if (isset($j['kv']) && is_array($j['kv'])) {
foreach ($j['kv'] as $k=>$v) $pairs[]=[(string)$k, json_encode($v, JSON_UNESCAPED_UNICODE)];
} elseif (isset($j['key'])) {
$pairs[]=[(string)$j['key'], json_encode($j['value'] ?? null, JSON_UNESCAPED_UNICODE)];
} else json_out(['error'=>'payload'], 400);
$st=$pdo->prepare(
"INSERT INTO state(device_id,key,value,updated_ts)
VALUES(:d,:k,:v,:t)
ON CONFLICT(device_id,key)
DO UPDATE SET value=excluded.value, updated_ts=excluded.updated_ts"
);
foreach ($pairs as $p) $st->execute([':d'=>$dev, ':k'=>$p[0], ':v'=>$p[1], ':t'=>$now]);
json_out(['ok'=>true,'updated'=>count($pairs)]);
}
/* ---------- STATE: GET ---------- */
if ($path === '/api/state/get' && $method === 'GET') {
$dev = $_GET['device'] ?? null; if (!$dev) json_out(['error'=>'device required'], 400);
// Admin OU device com token
if (!($ADMIN && (($_SERVER['HTTP_X_ADMIN_TOKEN'] ?? '') === $ADMIN))) {
require_token_dev($pdo);
}
$keys = isset($_GET['keys']) && $_GET['keys']!=='' ? explode(',', $_GET['keys']) : null;
if ($keys) {
$in = rtrim(str_repeat('?,', count($keys)), ',');
$st = $pdo->prepare("SELECT key,value,updated_ts FROM state WHERE device_id=? AND key IN ($in)");
$st->execute(array_merge([$dev], $keys));
} else {
$st = $pdo->prepare("SELECT key,value,updated_ts FROM state WHERE device_id=?");
$st->execute([$dev]);
}
$out=[];
while ($r=$st->fetch()) $out[$r['key']] = json_decode($r['value'], true);
json_out(['device'=>$dev,'kv'=>$out]);
}
/* ---------- COMMANDS: PUSH (admin) ---------- */
if ($path === '/api/commands/push' && $method === 'POST') {
require_admin_token($ADMIN);
$raw=file_get_contents('php://input') ?: '';
$j = json_decode($raw, true);
if (!is_array($j) || empty($j['device']) || empty($j['cmd'])) json_out(['error'=>'device/cmd required'], 400);
$st=$pdo->prepare("INSERT INTO command(device_id,ts,cmd,params,status) VALUES(:d,:t,:c,:p,:s)");
$st->execute([
':d'=>$j['device'], ':t'=>time(), ':c'=>$j['cmd'],
':p'=>isset($j['params']) ? json_encode($j['params'], JSON_UNESCAPED_UNICODE) : null,
':s'=>'queued'
]);
json_out(['queued'=>true,'id'=>$pdo->lastInsertId()]);
}
/* ---------- COMMANDS: POLL (device) ---------- */
if ($path === '/api/commands/poll' && $method === 'GET') {
$dev = require_token_dev($pdo);
$st=$pdo->prepare("SELECT id,cmd,params,ts FROM command WHERE device_id=:d AND status='queued' ORDER BY id ASC LIMIT 10");
$st->execute([':d'=>$dev]);
$rows=$st->fetchAll();
foreach ($rows as &$r) $r['params'] = $r['params'] ? json_decode($r['params'], true) : (object)[];
json_out(['device'=>$dev,'commands'=>$rows]);
}
/* ---------- COMMANDS: ACK (robusto) ---------- */
if ($path === '/api/commands/ack' && $method === 'POST') {
// valida token se quiseres (mas não uses device_id porque a tabela não tem)
require_token_dev($pdo);
$raw = file_get_contents('php://input') ?: '';
$j = json_decode($raw, true) ?: [];
if (empty($j['id'])) json_out(['error' => 'id required'], 400);
// interpreta "success"/"ok"/"status" de forma robusta
$val = $j['success'] ?? $j['ok'] ?? $j['status'] ?? true;
if (is_bool($val)) {
$ok = $val;
} elseif (is_numeric($val)) {
$ok = ((int)$val) === 1;
} elseif (is_string($val)) {
$ok = in_array(strtolower(trim($val)), ['1','true','ok','done','yes','y'], true);
} else {
$ok = true; // default otimista
}
// se tiveres coluna ack_ts, ativa a linha correspondente
$sql = "UPDATE commands
SET status = :status
WHERE id = :i";
$args = [':status' => ($ok ? 'done' : 'failed'), ':i' => (int)$j['id']];
// descomenta se existir a coluna ack_ts
// $sql = "UPDATE commands SET status=:status, ack_ts=:a WHERE id=:i";
// $args[':a'] = time();
$st = $pdo->prepare($sql);
$st->execute($args);
json_out(['updated' => $st->rowCount(), 'id' => (int)$j['id'], 'success' => $ok]);
}
/* ---------- 404 ---------- */
json_out(['error'=>'not found','path'=>$path], 404);

BIN
api/iot.sqlite Normal file

Binary file not shown.

1
api/listener.lock Normal file
View File

@ -0,0 +1 @@
51793

228
api/listener_recovery.php Normal file
View File

@ -0,0 +1,228 @@
<?php
declare(strict_types=1);
define('LOG_ERROR', 0);
define('LOG_INFO', 1);
define('LOG_DEBUG', 2);
$LOG_LEVEL = LOG_DEBUG;
//$LOG_LEVEL = LOG_INFO;
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/mqtt-listener/recovery-error.log');
set_time_limit(0);
ignore_user_abort(true);
function logmsg(string $msg, int $level = LOG_INFO): void {
global $LOG_LEVEL;
if ($level > $LOG_LEVEL) return;
echo '[' . date('Y-m-d H:i:s') . "] $msg\n";
if (function_exists('flush')) @flush();
}
require __DIR__ . '/../vendor/autoload.php';
use PhpMqtt\Client\MqttClient;
use PhpMqtt\Client\ConnectionSettings;
// DB
$db = new PDO(
"mysql:host=localhost;dbname=cagalhao;charset=utf8",
"master",
"master",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// RECOVERY BROKER
$recoveryHost = 'mqtt-recovery.xupas.mywire.org';
$recoveryPort = 8884;
$recoveryUser = 'recovery';
$recoveryPass = 'recovery123';
// MAIN BROKER TEST
$mainHost = 'mqtt.xupas.mywire.org';
$mainPort = 8883;
// Cert principal a enviar aos ESPs
$certPath = "/etc/mosquitto/certs/server.crt";
// MQTT recovery settings
$settings = (new ConnectionSettings())
->setUsername($recoveryUser)
->setPassword($recoveryPass)
->setUseTls(true)
->setTlsSelfSignedAllowed(true)
->setKeepAliveInterval(30)
->setConnectTimeout(5)
->setSocketTimeout(1)
->setReconnectAutomatically(true);
$mqtt = new MqttClient(
$recoveryHost,
$recoveryPort,
'listener-recovery-' . getmypid()
);
function main_broker_online(string $host, int $port): bool {
$fp = @fsockopen($host, $port, $errno, $errstr, 2);
if ($fp) {
fclose($fp);
return true;
}
return false;
}
function send_switch_primary(MqttClient $mqtt, PDO $db): void {
$stmt = $db->query("
SELECT uid
FROM contadores
WHERE online=1
");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$uid = $row['uid'];
$mqtt->publish(
"esp/$uid/cmd",
json_encode(['cmd' => 'switch_primary']),
1
);
logmsg("SWITCH_PRIMARY enviado para $uid");
}
}
function send_cert_update(MqttClient $mqtt, string $uid, string $certPath): void {
if (!file_exists($certPath)) {
logmsg("Cert não encontrado: $certPath", LOG_ERROR);
return;
}
$cert = file_get_contents($certPath);
$mqtt->publish(
"esp/$uid/cmd",
json_encode([
'cmd' => 'UPDATE_CERT',
'pem' => $cert
]),
1
);
logmsg("UPDATE_CERT enviado para $uid");
}
$subscribe = function () use (
$mqtt,
$db,
$certPath,
$mainHost,
$mainPort
) {
$mqtt->subscribe(
'esp/+/status',
function (string $topic, string $msg)
use ($db, $mqtt, $mainHost, $mainPort) {
$uid = explode('/', $topic)[1] ?? null;
if (!$uid) return;
$stmt = $db->prepare("
INSERT INTO contadores
(uid, entradas, saidas, entradas_total, saidas_total, last_seen, online)
VALUES
(:uid,0,0,0,0,NOW(),1)
ON DUPLICATE KEY UPDATE
last_seen=NOW(),
online=1
");
$stmt->execute(['uid' => $uid]);
logmsg("RECOVERY STATUS $uid => $msg");
static $lastCheck = 0;
if (time() - $lastCheck >= 15) {
$lastCheck = time();
logmsg("CHECK broker");
if (main_broker_online($mainHost, $mainPort)) {
logmsg("MAIN broker online");
send_switch_primary($mqtt, $db);
} else {
logmsg("MAIN broker offline");
}
}
},
0
);
$mqtt->subscribe('esp/+/event', function (string $topic, string $msg) use ($db) {
$uid = explode('/', $topic)[1] ?? null;
if (!$uid) return;
logmsg("RECOVERY EVENT $uid => $msg");
}, 0);
$mqtt->subscribe('esp/+/cert_request', function (string $topic, string $msg) use ($mqtt, $certPath) {
$uid = explode('/', $topic)[1] ?? null;
if (!$uid) return;
logmsg("CERT_REQUEST de $uid");
send_cert_update($mqtt, $uid, $certPath);
}, 0);
};
while (true) {
try {
logmsg("Ligar recovery $recoveryHost:$recoveryPort");
$mqtt->connect($settings, false);
$subscribe();
logmsg("RECOVERY ligado e subscrito");
$nextMainCheck = time() + 10;
while ($mqtt->isConnected()) {
$mqtt->loop(false);
logmsg("tick");
if (time() >= $nextMainCheck) {
$nextMainCheck = time() + 15;
logmsg("LOOP recovery", LOG_DEBUG);
if (main_broker_online($mainHost, $mainPort)) {
logmsg("MAIN broker online");
logmsg("MAIN broker online. Mandar switch_primary.");
send_switch_primary($mqtt, $db);
} else {
logmsg("MAIN broker ainda offline", LOG_DEBUG);
}
}
usleep(100000);
}
$mqtt->disconnect();
} catch (Throwable $e) {
logmsg("ERRO recovery: " . $e->getMessage(), LOG_ERROR);
sleep(5);
}
}

1
api/listener_test.lock Normal file
View File

@ -0,0 +1 @@
51217

31
api/login.php Normal file
View File

@ -0,0 +1,31 @@
<?php session_start(); ?>
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
<title>Login</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light d-flex align-items-center" style="height:100vh;">
<div class="container text-center">
<div class="row justify-content-center">
<div class="col-md-4">
<div class="card shadow">
<div class="card-body">
<h4 class="mb-4">Login 🔐</h4>
<form method="POST" action="validar.php">
<div class="mb-3">
<input type="text" name="username" class="form-control" placeholder="Utilizador" required>
</div>
<div class="mb-3">
<input type="password" name="password" class="form-control" placeholder="Senha" required>
</div>
<button type="submit" class="btn btn-primary w-100">Entrar</button>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

5
api/logout.php Normal file
View File

@ -0,0 +1,5 @@
<?php
session_start();
session_destroy();
header("Location: login.php");
exit;

160
api/menu.php Normal file
View File

@ -0,0 +1,160 @@
<?php
require_once "protecao.php";
/* =========================
CONTROLO DA VPN
========================= */
$outputVPN = null;
if (isset($_GET['vpn'])) {
$allowed = ['md','al','nl','de','ch','status'];
$cmd = $_GET['vpn'];
if (in_array($cmd, $allowed)) {
// IP do LXC NordVPN (104)
$vpnHost = "192.168.10.104";
$vpnUser = "root";
$sshCmd = "/usr/bin/ssh -i /var/www/.ssh/nordvpn "
. "-o BatchMode=yes -o StrictHostKeyChecking=no "
. "{$vpnUser}@{$vpnHost} "
. escapeshellarg($cmd)
. " 2>&1";
$outputVPN = shell_exec($sshCmd);
} else {
$outputVPN = "❌ Comando VPN inválido.";
}
}
?>
<!DOCTYPE html>
<html lang="pt">
<head>
<meta charset="UTF-8">
<title>Menu Principal</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #0f0f0f;
color: #e0e0e0;
font-family: 'Segoe UI', sans-serif;
}
h2, h4 {
color: #00ffc8;
text-shadow: 0 0 6px #00ffc8;
}
.btn {
border-width: 2px;
font-weight: 600;
padding: 8px 14px;
font-size: 0.9rem;
transition: all 0.15s ease-in-out;
}
.btn:hover {
box-shadow: 0 0 8px currentColor;
}
pre {
background: #000;
color: #00ff99;
padding: 12px;
border-radius: 6px;
font-size: 0.85rem;
text-align: left;
}
.container {
max-width: 720px;
}
footer {
margin-top: 50px;
font-size: 0.85rem;
color: #777;
}
</style>
</head>
<body class="p-4">
<div class="container text-center">
<h2 class="mb-4">
Bem-vindo, <?= htmlspecialchars($_SESSION['username']) ?> 😎
</h2>
<!-- MENU PRINCIPAL -->
<div class="row g-2 mb-5">
<div class="col-12">
<a href="../console.php" class="btn btn-outline-light w-100">📊 Estado dos Dispositivos</a>
</div>
<div class="col-12">
<a href="../comandos.php" class="btn btn-outline-light w-100">🛠️ Enviar Comando</a>
</div>
<div class="col-12">
<a href="../historico.php" class="btn btn-outline-light w-100">📜 Ver Histórico</a>
</div>
<div class="col-12">
<a href="config.php" class="btn btn-outline-info w-100">⚙️ Configurações</a>
</div>
<div class="col-12">
<a href="https://web.xupas.mywire.org/matrix_admin.php" target="_blank"
class="btn btn-outline-success w-100">
💬 Matrix Admin (Web)
</a>
</div>
<div class="col-12">
<a href="https://admmatrix.xupas.mywire.org" target="_blank"
class="btn btn-outline-warning w-100">
🧠 Synapse Admin (Servidor)
</a>
</div>
</div>
<!-- CONTROLO DA VPN -->
<hr class="my-4">
<h4 class="mb-3">🌍 Controlo da VPN</h4>
<div class="row g-2 mb-4">
<div class="col-12 col-md-6">
<a href="?vpn=md" class="btn btn-outline-success w-100">🇲🇩 Moldova (YouTube)</a>
</div>
<div class="col-12 col-md-6">
<a href="?vpn=al" class="btn btn-outline-success w-100">🇦🇱 Albania (Ad-free)</a>
</div>
<div class="col-12 col-md-6">
<a href="?vpn=nl" class="btn btn-outline-info w-100">🇳🇱 Netherlands (Torrent)</a>
</div>
<div class="col-12 col-md-6">
<a href="?vpn=de" class="btn btn-outline-info w-100">🇩🇪 Germany (Speed)</a>
</div>
<div class="col-12">
<a href="?vpn=status" class="btn btn-outline-light w-100">📡 Ver Estado VPN</a>
</div>
</div>
<?php if ($outputVPN): ?>
<div class="mb-4">
<pre><?= htmlspecialchars($outputVPN) ?></pre>
</div>
<?php endif; ?>
<!-- LOGOUT -->
<div class="row">
<div class="col-12">
<a href="logout.php" class="btn btn-outline-danger w-100">🚪 Terminar Sessão</a>
</div>
</div>
<footer class="text-center mt-5">
<p>© <?= date('Y') ?> Xupas Systems | Todos os direitos reservados</p>
</footer>
</div>
</body>
</html>

11
api/mqtt.php Normal file
View File

@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
function mqtt_publish(string $topic, string $msg): void {
$cmd = sprintf(
'mosquitto_pub -h 127.0.0.1 -p 1883 -t %s -m %s',
escapeshellarg($topic),
escapeshellarg($msg)
);
shell_exec($cmd . " > /dev/null 2>&1");
}

236
api/mqtt_listener.php Normal file
View File

@ -0,0 +1,236 @@
<?php
declare(strict_types=1);
/* =========================
* LOG LEVELS
* ========================= */
if (!defined('LOG_ERROR')) {
define('LOG_ERROR', 0);
}
if (!defined('LOG_INFO')) {
define('LOG_INFO', 1);
}
if (!defined('LOG_DEBUG')) {
define('LOG_DEBUG', 2);
}
$LOG_LEVEL = LOG_INFO;
/* =========================
* PHP RUNTIME
* ========================= */
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/mqtt-listener/php-error.log');
set_time_limit(0);
ignore_user_abort(true);
/* =========================
* HELPERS
* ========================= */
function logmsg(string $msg, int $level = LOG_INFO): void {
global $LOG_LEVEL;
if ($level > $LOG_LEVEL) return;
echo '[' . date('Y-m-d H:i:s') . "] $msg\n";
if (function_exists('flush')) @flush();
}
/* =========================
* AUTOLOAD
* ========================= */
require __DIR__ . '/../vendor/autoload.php';
use PhpMqtt\Client\MqttClient;
use PhpMqtt\Client\ConnectionSettings;
/* =========================
* DATABASE
* ========================= */
$db = new PDO(
"mysql:host=localhost;dbname=cagalhao;charset=utf8",
"master",
"master",
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
/* =========================
* MQTT CONFIG
* ========================= */
$mqttHost = 'mqtt.xupas.mywire.org';
$mqttUser = 'xupa';
$mqttPass = 'xupa';
$bootstrapSecret = "XUPA_TEMP_SECRET_2026";
$certPath = "/etc/mosquitto/certs/server.crt";
/* =========================
* TLS CLIENT (8883)
* ========================= */
$tlsSettings = (new ConnectionSettings())
->setUsername($mqttUser)
->setPassword($mqttPass)
->setUseTls(true)
->setTlsSelfSignedAllowed(true)
->setKeepAliveInterval(30)
->setConnectTimeout(5)
->setSocketTimeout(1)
->setReconnectAutomatically(true);
$mqttTls = new MqttClient($mqttHost, 8883, 'listener-php');
/* =========================
* BOOTSTRAP CLIENT (1883)
* ========================= */
$bootstrapSettings = (new ConnectionSettings())
->setUsername($mqttUser)
->setPassword($mqttPass)
->setUseTls(false)
->setKeepAliveInterval(30)
->setReconnectAutomatically(true);
$mqttBootstrap = new MqttClient($mqttHost, 1883, 'listener-bootstrap');
/* =========================
* SUBSCRIPTIONS TLS
* ========================= */
$subscribeTls = function () use ($mqttTls, $db) {
// =========================
// STATUS ESP
// =========================
$mqttTls->subscribe('esp/+/status', function (string $topic, string $msg) use ($db) {
$uid = explode('/', $topic)[1] ?? null;
if (!$uid) return;
$stmt = $db->prepare("
INSERT INTO contadores (uid, entradas, saidas, entradas_total, saidas_total, last_seen, online)
VALUES (:uid,0,0,0,0,NOW(),1)
ON DUPLICATE KEY UPDATE last_seen=NOW(), online=1
");
$stmt->execute(['uid' => $uid]);
logmsg("STATUS $uid");
}, 0);
// =========================
// PEDIDO DE HORA GLOBAL
// =========================
$mqttTls->subscribe('time/request', function (string $topic, string $msg) use ($mqttTls) {
logmsg("Pedido global de hora");
$payload = json_encode([
'h' => (int)date('H'),
'm' => (int)date('i')
]);
$mqttTls->publish('time/now', $payload, 0);
logmsg("Hora enviada: $payload");
}, 0);
// =========================
// PEDIDO DE HORA POR DEVICE
// time/request/esp_xxxxxx
// =========================
$mqttTls->subscribe('time/request/+', function (string $topic, string $msg) use ($mqttTls) {
$uid = explode('/', $topic)[2] ?? null;
if (!$uid) return;
logmsg("Pedido hora de $uid");
$payload = json_encode([
'h' => (int)date('H'),
'm' => (int)date('i')
]);
$mqttTls->publish("time/now/$uid", $payload, 0);
logmsg("Hora enviada para $uid");
}, 0);
};
/* =========================
* SUBSCRIPTIONS BOOTSTRAP
* ========================= */
$subscribeBootstrap = function () use ($mqttBootstrap, $bootstrapSecret, $certPath) {
$mqttBootstrap->subscribe('esp/+/bootstrap_request', function (string $topic, string $msg)
use ($mqttBootstrap, $bootstrapSecret, $certPath) {
$uid = explode('/', $topic)[1] ?? null;
if (!$uid) return;
logmsg("BOOTSTRAP pedido de $uid");
if (!file_exists($certPath)) {
logmsg("ERRO: certificado não encontrado", LOG_ERROR);
return;
}
$cert = file_get_contents($certPath);
$certB64 = base64_encode($cert);
$ts = time();
$dataToSign = $certB64 . $ts;
$hmac = hash_hmac('sha256', $dataToSign, $bootstrapSecret);
$payload = json_encode([
'cmd' => 'update_cert',
'cert' => $certB64,
'ts' => $ts,
'hmac' => $hmac
]);
$mqttBootstrap->publish("esp/$uid/bootstrap_response", $payload, 0);
logmsg("BOOTSTRAP enviado para $uid");
}, 0);
};
/* =========================
* MAIN LOOP
* ========================= */
while (true) {
try {
logmsg("Ligando TLS 8883...");
$mqttTls->connect($tlsSettings, false);
$subscribeTls();
logmsg("TLS ligado");
logmsg("Ligando Bootstrap 1883...");
$mqttBootstrap->connect($bootstrapSettings, false);
$subscribeBootstrap();
logmsg("Bootstrap ligado");
while ($mqttTls->isConnected() && $mqttBootstrap->isConnected()) {
$mqttTls->loop(true, false);
$mqttBootstrap->loop(true, false);
usleep(100000);
}
$mqttTls->disconnect();
$mqttBootstrap->disconnect();
} catch (Throwable $e) {
logmsg("Erro: " . $e->getMessage(), LOG_ERROR);
sleep(5);
}
}

7
api/new-device.php Normal file
View File

@ -0,0 +1,7 @@
<?php declare(strict_types=1);
require __DIR__.'/bootstrap.php'; $pdo=db();
$id=$argv[1]??null; if(!$id){fwrite(STDERR,"uso: php new-device.php DEVICE_ID\n"); exit(2);}
$token=bin2hex(random_bytes(24));
$pdo->prepare("INSERT OR REPLACE INTO device(id,token,created_at) VALUES(:i,:t,datetime('now'))")
->execute([':i'=>$id,':t'=>$token]);
echo "device=$id token=$token\n";

8
api/protecao.php Normal file
View File

@ -0,0 +1,8 @@
<?php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
header("Location: login.php");
exit;
}

44
api/reset_parciais.php Normal file
View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/**
* Reset de parciais com acumulação em totais (por máquina):
* total += parcial; parcial = 0
*/
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED & ~E_NOTICE & ~E_USER_NOTICE);
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
try {
$db = new PDO('mysql:host=localhost;dbname=cagalhao;charset=utf8', 'master', 'master', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
// garantir colunas (ignora se já existirem)
$db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS entradas_total INT NOT NULL DEFAULT 0");
$db->exec("ALTER TABLE contadores ADD COLUMN IF NOT EXISTS saidas_total INT NOT NULL DEFAULT 0");
$uid = $_POST['uid'] ?? '';
if ($uid === '') {
header("Location: /console.php?ok=err");
exit;
}
$stmt = $db->prepare("
UPDATE contadores
SET entradas_total = COALESCE(entradas_total,0) + COALESCE(entradas,0),
saidas_total = COALESCE(saidas_total,0) + COALESCE(saidas,0),
entradas = 0,
saidas = 0
WHERE uid = :uid
");
$stmt->execute(['uid' => $uid]);
header("Location: /console.php?ok=line");
exit;
} catch (Throwable $e) {
header("Location: /console.php?ok=err");
exit;
}

11
api/reset_pass.php Normal file
View File

@ -0,0 +1,11 @@
<?php
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$newPassword = password_hash("admin123", PASSWORD_DEFAULT);
$stmt = $pdo->prepare("UPDATE users SET password_hash = ? WHERE username = 'admin'");
$stmt->execute([$newPassword]);
echo "✅ Password de admin alterada para admin123";

34
api/send_cmd.php Normal file
View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json');
require __DIR__ . '/db.php'; // ligação PDO
require __DIR__ . '/mqtt.php'; // função mqtt_publish($topic,$msg)
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || !isset($data['uid']) || !isset($data['cmd'])) {
http_response_code(400);
echo json_encode(['error'=>'invalid_request']);
exit;
}
$uid = $data['uid'];
$cmd = $data['cmd'];
$value = $data['value'] ?? null;
// ----------------------- MQTT -----------------------
$topic = "esp/$uid/cmd";
$msg = ['cmd' => $cmd];
if ($value !== null) $msg['value'] = $value;
mqtt_publish($topic, json_encode($msg, JSON_UNESCAPED_SLASHES));
// ----------------------- BD -------------------------
if ($cmd === 'BLOCK') {
$stmt = $db->prepare("UPDATE contadores SET blocked = ? WHERE uid = ?");
$stmt->execute([intval($value), $uid]);
}
echo json_encode(['ok'=>1]);

23
api/sniff_once.php Normal file
View File

@ -0,0 +1,23 @@
<?php
set_time_limit(30);
require __DIR__ . '/vendor/autoload.php';
use PhpMqtt\Client\MqttClient;
use PhpMqtt\Client\ConnectionSettings;
$data = [];
$mqtt = new MqttClient('xupas.mooo.com', 1883, 'web-sniffer-'.rand());
$settings = (new ConnectionSettings)->setUsername('xupa')->setPassword('xupa');
$mqtt->connect($settings, true);
$mqtt->subscribe('esp/+/event', function($topic, $message) use (&$data, $mqtt) {
$data[] = ['topic'=>$topic, 'message'=>$message];
if (count($data) >= 5) {
$mqtt->interrupt(); // para o loop depois de 5 msg
}
}, 0);
$mqtt->loop(true);
header('Content-Type: application/json');
echo json_encode($data, JSON_PRETTY_PRINT);

42
api/validar.php Normal file
View File

@ -0,0 +1,42 @@
<?php
session_start();
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
if (!isset($_POST['username'], $_POST['password'])) {
die("❌ Dados não recebidos via POST. Verifica o formulário.");
}
try {
$pdo = new PDO("mysql:host=localhost;dbname=cagalhao;charset=utf8mb4", "master", "master", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$_POST['username']]);
$user = $stmt->fetch();
if (!$user) {
echo "<script>alert('❌ Utilizador não encontrado.');window.location='login.php';</script>";
exit;
}
if (!password_verify($_POST['password'], $user['password_hash'])) {
echo "<script>alert('❌ Password incorreta.');window.location='login.php';</script>";
exit;
}
// ✅ Login correto
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
// Redireciona para o menu principal
header("Location: menu.php");
exit;
} catch (Exception $e) {
echo "<p>💥 Erro na base de dados: " . htmlspecialchars($e->getMessage()) . "</p>";
}
?>

25
api/vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit16956408defe802b10d9eb0ff0557782::getLoader();

585
api/vendor/composer/ClassLoader.php vendored Normal file
View File

@ -0,0 +1,585 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var ?string */
private $vendorDir;
// PSR-4
/**
* @var array[]
* @psalm-var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, array<int, string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* @var array[]
* @psalm-var array<string, array<string, string[]>>
*/
private $prefixesPsr0 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var string[]
* @psalm-var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var bool[]
* @psalm-var array<string, bool>
*/
private $missingClasses = array();
/** @var ?string */
private $apcuPrefix;
/**
* @var self[]
*/
private static $registeredLoaders = array();
/**
* @param ?string $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return string[]
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array[]
* @psalm-return array<string, array<int, string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return string[] Array of classname => path
* @psalm-return array<string, string>
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param string[] $classMap Class to filename map
* @psalm-param array<string, string> $classMap
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders indexed by their corresponding vendor directories.
*
* @return self[]
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

View File

@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}

19
api/vendor/composer/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,54 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'MyCLabs\\Enum\\Enum' => $vendorDir . '/myclabs/php-enum/src/Enum.php',
'MyCLabs\\Enum\\PHPUnit\\Comparator' => $vendorDir . '/myclabs/php-enum/src/PHPUnit/Comparator.php',
'PhpMqtt\\Client\\Concerns\\GeneratesRandomClientIds' => $vendorDir . '/php-mqtt/client/src/Concerns/GeneratesRandomClientIds.php',
'PhpMqtt\\Client\\Concerns\\OffersHooks' => $vendorDir . '/php-mqtt/client/src/Concerns/OffersHooks.php',
'PhpMqtt\\Client\\Concerns\\TranscodesData' => $vendorDir . '/php-mqtt/client/src/Concerns/TranscodesData.php',
'PhpMqtt\\Client\\Concerns\\ValidatesConfiguration' => $vendorDir . '/php-mqtt/client/src/Concerns/ValidatesConfiguration.php',
'PhpMqtt\\Client\\Concerns\\WorksWithBuffers' => $vendorDir . '/php-mqtt/client/src/Concerns/WorksWithBuffers.php',
'PhpMqtt\\Client\\ConnectionSettings' => $vendorDir . '/php-mqtt/client/src/ConnectionSettings.php',
'PhpMqtt\\Client\\Contracts\\MessageProcessor' => $vendorDir . '/php-mqtt/client/src/Contracts/MessageProcessor.php',
'PhpMqtt\\Client\\Contracts\\MqttClient' => $vendorDir . '/php-mqtt/client/src/Contracts/MqttClient.php',
'PhpMqtt\\Client\\Contracts\\Repository' => $vendorDir . '/php-mqtt/client/src/Contracts/Repository.php',
'PhpMqtt\\Client\\Exceptions\\ClientNotConnectedToBrokerException' => $vendorDir . '/php-mqtt/client/src/Exceptions/ClientNotConnectedToBrokerException.php',
'PhpMqtt\\Client\\Exceptions\\ConfigurationInvalidException' => $vendorDir . '/php-mqtt/client/src/Exceptions/ConfigurationInvalidException.php',
'PhpMqtt\\Client\\Exceptions\\ConnectingToBrokerFailedException' => $vendorDir . '/php-mqtt/client/src/Exceptions/ConnectingToBrokerFailedException.php',
'PhpMqtt\\Client\\Exceptions\\DataTransferException' => $vendorDir . '/php-mqtt/client/src/Exceptions/DataTransferException.php',
'PhpMqtt\\Client\\Exceptions\\InvalidMessageException' => $vendorDir . '/php-mqtt/client/src/Exceptions/InvalidMessageException.php',
'PhpMqtt\\Client\\Exceptions\\MqttClientException' => $vendorDir . '/php-mqtt/client/src/Exceptions/MqttClientException.php',
'PhpMqtt\\Client\\Exceptions\\PendingMessageAlreadyExistsException' => $vendorDir . '/php-mqtt/client/src/Exceptions/PendingMessageAlreadyExistsException.php',
'PhpMqtt\\Client\\Exceptions\\PendingMessageNotFoundException' => $vendorDir . '/php-mqtt/client/src/Exceptions/PendingMessageNotFoundException.php',
'PhpMqtt\\Client\\Exceptions\\ProtocolNotSupportedException' => $vendorDir . '/php-mqtt/client/src/Exceptions/ProtocolNotSupportedException.php',
'PhpMqtt\\Client\\Exceptions\\ProtocolViolationException' => $vendorDir . '/php-mqtt/client/src/Exceptions/ProtocolViolationException.php',
'PhpMqtt\\Client\\Exceptions\\RepositoryException' => $vendorDir . '/php-mqtt/client/src/Exceptions/RepositoryException.php',
'PhpMqtt\\Client\\Logger' => $vendorDir . '/php-mqtt/client/src/Logger.php',
'PhpMqtt\\Client\\Message' => $vendorDir . '/php-mqtt/client/src/Message.php',
'PhpMqtt\\Client\\MessageProcessors\\BaseMessageProcessor' => $vendorDir . '/php-mqtt/client/src/MessageProcessors/BaseMessageProcessor.php',
'PhpMqtt\\Client\\MessageProcessors\\Mqtt311MessageProcessor' => $vendorDir . '/php-mqtt/client/src/MessageProcessors/Mqtt311MessageProcessor.php',
'PhpMqtt\\Client\\MessageProcessors\\Mqtt31MessageProcessor' => $vendorDir . '/php-mqtt/client/src/MessageProcessors/Mqtt31MessageProcessor.php',
'PhpMqtt\\Client\\MessageType' => $vendorDir . '/php-mqtt/client/src/MessageType.php',
'PhpMqtt\\Client\\MqttClient' => $vendorDir . '/php-mqtt/client/src/MqttClient.php',
'PhpMqtt\\Client\\PendingMessage' => $vendorDir . '/php-mqtt/client/src/PendingMessage.php',
'PhpMqtt\\Client\\PublishedMessage' => $vendorDir . '/php-mqtt/client/src/PublishedMessage.php',
'PhpMqtt\\Client\\Repositories\\MemoryRepository' => $vendorDir . '/php-mqtt/client/src/Repositories/MemoryRepository.php',
'PhpMqtt\\Client\\SubscribeRequest' => $vendorDir . '/php-mqtt/client/src/SubscribeRequest.php',
'PhpMqtt\\Client\\Subscription' => $vendorDir . '/php-mqtt/client/src/Subscription.php',
'PhpMqtt\\Client\\UnsubscribeRequest' => $vendorDir . '/php-mqtt/client/src/UnsubscribeRequest.php',
'Psr\\Log\\AbstractLogger' => $vendorDir . '/psr/log/src/AbstractLogger.php',
'Psr\\Log\\InvalidArgumentException' => $vendorDir . '/psr/log/src/InvalidArgumentException.php',
'Psr\\Log\\LogLevel' => $vendorDir . '/psr/log/src/LogLevel.php',
'Psr\\Log\\LoggerAwareInterface' => $vendorDir . '/psr/log/src/LoggerAwareInterface.php',
'Psr\\Log\\LoggerAwareTrait' => $vendorDir . '/psr/log/src/LoggerAwareTrait.php',
'Psr\\Log\\LoggerInterface' => $vendorDir . '/psr/log/src/LoggerInterface.php',
'Psr\\Log\\LoggerTrait' => $vendorDir . '/psr/log/src/LoggerTrait.php',
'Psr\\Log\\NullLogger' => $vendorDir . '/psr/log/src/NullLogger.php',
'Stringable' => $vendorDir . '/myclabs/php-enum/stubs/Stringable.php',
);

View File

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

12
api/vendor/composer/autoload_psr4.php vendored Normal file
View File

@ -0,0 +1,12 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
'PhpMqtt\\Client\\' => array($vendorDir . '/php-mqtt/client/src'),
'MyCLabs\\Enum\\' => array($vendorDir . '/myclabs/php-enum/src'),
);

38
api/vendor/composer/autoload_real.php vendored Normal file
View File

@ -0,0 +1,38 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit16956408defe802b10d9eb0ff0557782
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit16956408defe802b10d9eb0ff0557782', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit16956408defe802b10d9eb0ff0557782', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit16956408defe802b10d9eb0ff0557782::getInitializer($loader));
$loader->register(true);
return $loader;
}
}

93
api/vendor/composer/autoload_static.php vendored Normal file
View File

@ -0,0 +1,93 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit16956408defe802b10d9eb0ff0557782
{
public static $prefixLengthsPsr4 = array (
'P' =>
array (
'Psr\\Log\\' => 8,
'PhpMqtt\\Client\\' => 15,
),
'M' =>
array (
'MyCLabs\\Enum\\' => 13,
),
);
public static $prefixDirsPsr4 = array (
'Psr\\Log\\' =>
array (
0 => __DIR__ . '/..' . '/psr/log/src',
),
'PhpMqtt\\Client\\' =>
array (
0 => __DIR__ . '/..' . '/php-mqtt/client/src',
),
'MyCLabs\\Enum\\' =>
array (
0 => __DIR__ . '/..' . '/myclabs/php-enum/src',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'MyCLabs\\Enum\\Enum' => __DIR__ . '/..' . '/myclabs/php-enum/src/Enum.php',
'MyCLabs\\Enum\\PHPUnit\\Comparator' => __DIR__ . '/..' . '/myclabs/php-enum/src/PHPUnit/Comparator.php',
'PhpMqtt\\Client\\Concerns\\GeneratesRandomClientIds' => __DIR__ . '/..' . '/php-mqtt/client/src/Concerns/GeneratesRandomClientIds.php',
'PhpMqtt\\Client\\Concerns\\OffersHooks' => __DIR__ . '/..' . '/php-mqtt/client/src/Concerns/OffersHooks.php',
'PhpMqtt\\Client\\Concerns\\TranscodesData' => __DIR__ . '/..' . '/php-mqtt/client/src/Concerns/TranscodesData.php',
'PhpMqtt\\Client\\Concerns\\ValidatesConfiguration' => __DIR__ . '/..' . '/php-mqtt/client/src/Concerns/ValidatesConfiguration.php',
'PhpMqtt\\Client\\Concerns\\WorksWithBuffers' => __DIR__ . '/..' . '/php-mqtt/client/src/Concerns/WorksWithBuffers.php',
'PhpMqtt\\Client\\ConnectionSettings' => __DIR__ . '/..' . '/php-mqtt/client/src/ConnectionSettings.php',
'PhpMqtt\\Client\\Contracts\\MessageProcessor' => __DIR__ . '/..' . '/php-mqtt/client/src/Contracts/MessageProcessor.php',
'PhpMqtt\\Client\\Contracts\\MqttClient' => __DIR__ . '/..' . '/php-mqtt/client/src/Contracts/MqttClient.php',
'PhpMqtt\\Client\\Contracts\\Repository' => __DIR__ . '/..' . '/php-mqtt/client/src/Contracts/Repository.php',
'PhpMqtt\\Client\\Exceptions\\ClientNotConnectedToBrokerException' => __DIR__ . '/..' . '/php-mqtt/client/src/Exceptions/ClientNotConnectedToBrokerException.php',
'PhpMqtt\\Client\\Exceptions\\ConfigurationInvalidException' => __DIR__ . '/..' . '/php-mqtt/client/src/Exceptions/ConfigurationInvalidException.php',
'PhpMqtt\\Client\\Exceptions\\ConnectingToBrokerFailedException' => __DIR__ . '/..' . '/php-mqtt/client/src/Exceptions/ConnectingToBrokerFailedException.php',
'PhpMqtt\\Client\\Exceptions\\DataTransferException' => __DIR__ . '/..' . '/php-mqtt/client/src/Exceptions/DataTransferException.php',
'PhpMqtt\\Client\\Exceptions\\InvalidMessageException' => __DIR__ . '/..' . '/php-mqtt/client/src/Exceptions/InvalidMessageException.php',
'PhpMqtt\\Client\\Exceptions\\MqttClientException' => __DIR__ . '/..' . '/php-mqtt/client/src/Exceptions/MqttClientException.php',
'PhpMqtt\\Client\\Exceptions\\PendingMessageAlreadyExistsException' => __DIR__ . '/..' . '/php-mqtt/client/src/Exceptions/PendingMessageAlreadyExistsException.php',
'PhpMqtt\\Client\\Exceptions\\PendingMessageNotFoundException' => __DIR__ . '/..' . '/php-mqtt/client/src/Exceptions/PendingMessageNotFoundException.php',
'PhpMqtt\\Client\\Exceptions\\ProtocolNotSupportedException' => __DIR__ . '/..' . '/php-mqtt/client/src/Exceptions/ProtocolNotSupportedException.php',
'PhpMqtt\\Client\\Exceptions\\ProtocolViolationException' => __DIR__ . '/..' . '/php-mqtt/client/src/Exceptions/ProtocolViolationException.php',
'PhpMqtt\\Client\\Exceptions\\RepositoryException' => __DIR__ . '/..' . '/php-mqtt/client/src/Exceptions/RepositoryException.php',
'PhpMqtt\\Client\\Logger' => __DIR__ . '/..' . '/php-mqtt/client/src/Logger.php',
'PhpMqtt\\Client\\Message' => __DIR__ . '/..' . '/php-mqtt/client/src/Message.php',
'PhpMqtt\\Client\\MessageProcessors\\BaseMessageProcessor' => __DIR__ . '/..' . '/php-mqtt/client/src/MessageProcessors/BaseMessageProcessor.php',
'PhpMqtt\\Client\\MessageProcessors\\Mqtt311MessageProcessor' => __DIR__ . '/..' . '/php-mqtt/client/src/MessageProcessors/Mqtt311MessageProcessor.php',
'PhpMqtt\\Client\\MessageProcessors\\Mqtt31MessageProcessor' => __DIR__ . '/..' . '/php-mqtt/client/src/MessageProcessors/Mqtt31MessageProcessor.php',
'PhpMqtt\\Client\\MessageType' => __DIR__ . '/..' . '/php-mqtt/client/src/MessageType.php',
'PhpMqtt\\Client\\MqttClient' => __DIR__ . '/..' . '/php-mqtt/client/src/MqttClient.php',
'PhpMqtt\\Client\\PendingMessage' => __DIR__ . '/..' . '/php-mqtt/client/src/PendingMessage.php',
'PhpMqtt\\Client\\PublishedMessage' => __DIR__ . '/..' . '/php-mqtt/client/src/PublishedMessage.php',
'PhpMqtt\\Client\\Repositories\\MemoryRepository' => __DIR__ . '/..' . '/php-mqtt/client/src/Repositories/MemoryRepository.php',
'PhpMqtt\\Client\\SubscribeRequest' => __DIR__ . '/..' . '/php-mqtt/client/src/SubscribeRequest.php',
'PhpMqtt\\Client\\Subscription' => __DIR__ . '/..' . '/php-mqtt/client/src/Subscription.php',
'PhpMqtt\\Client\\UnsubscribeRequest' => __DIR__ . '/..' . '/php-mqtt/client/src/UnsubscribeRequest.php',
'Psr\\Log\\AbstractLogger' => __DIR__ . '/..' . '/psr/log/src/AbstractLogger.php',
'Psr\\Log\\InvalidArgumentException' => __DIR__ . '/..' . '/psr/log/src/InvalidArgumentException.php',
'Psr\\Log\\LogLevel' => __DIR__ . '/..' . '/psr/log/src/LogLevel.php',
'Psr\\Log\\LoggerAwareInterface' => __DIR__ . '/..' . '/psr/log/src/LoggerAwareInterface.php',
'Psr\\Log\\LoggerAwareTrait' => __DIR__ . '/..' . '/psr/log/src/LoggerAwareTrait.php',
'Psr\\Log\\LoggerInterface' => __DIR__ . '/..' . '/psr/log/src/LoggerInterface.php',
'Psr\\Log\\LoggerTrait' => __DIR__ . '/..' . '/psr/log/src/LoggerTrait.php',
'Psr\\Log\\NullLogger' => __DIR__ . '/..' . '/psr/log/src/NullLogger.php',
'Stringable' => __DIR__ . '/..' . '/myclabs/php-enum/stubs/Stringable.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit16956408defe802b10d9eb0ff0557782::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit16956408defe802b10d9eb0ff0557782::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit16956408defe802b10d9eb0ff0557782::$classMap;
}, null, ClassLoader::class);
}
}

185
api/vendor/composer/installed.json vendored Normal file
View File

@ -0,0 +1,185 @@
{
"packages": [
{
"name": "myclabs/php-enum",
"version": "1.8.5",
"version_normalized": "1.8.5.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/php-enum.git",
"reference": "e7be26966b7398204a234f8673fdad5ac6277802"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/php-enum/zipball/e7be26966b7398204a234f8673fdad5ac6277802",
"reference": "e7be26966b7398204a234f8673fdad5ac6277802",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^7.3 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "1.*",
"vimeo/psalm": "^4.6.2 || ^5.2"
},
"time": "2025-01-14T11:49:03+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"MyCLabs\\Enum\\": "src/"
},
"classmap": [
"stubs/Stringable.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP Enum contributors",
"homepage": "https://github.com/myclabs/php-enum/graphs/contributors"
}
],
"description": "PHP Enum implementation",
"homepage": "https://github.com/myclabs/php-enum",
"keywords": [
"enum"
],
"support": {
"issues": "https://github.com/myclabs/php-enum/issues",
"source": "https://github.com/myclabs/php-enum/tree/1.8.5"
},
"funding": [
{
"url": "https://github.com/mnapoli",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum",
"type": "tidelift"
}
],
"install-path": "../myclabs/php-enum"
},
{
"name": "php-mqtt/client",
"version": "v2.2.0",
"version_normalized": "2.2.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-mqtt/client.git",
"reference": "8042ad93e72da8666e27168dc90670e45bdea274"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-mqtt/client/zipball/8042ad93e72da8666e27168dc90670e45bdea274",
"reference": "8042ad93e72da8666e27168dc90670e45bdea274",
"shasum": ""
},
"require": {
"myclabs/php-enum": "^1.7",
"php": "^8.0",
"psr/log": "^1.1|^2.0|^3.0"
},
"require-dev": {
"phpunit/php-invoker": "^3.0",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.5"
},
"suggest": {
"ext-redis": "Required for the RedisRepository"
},
"time": "2024-11-24T20:54:32+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"PhpMqtt\\Client\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Marvin Mall",
"email": "marvin-mall@msn.com",
"role": "developer"
}
],
"description": "An MQTT client written in and for PHP.",
"keywords": [
"client",
"mqtt",
"publish",
"subscribe"
],
"support": {
"issues": "https://github.com/php-mqtt/client/issues",
"source": "https://github.com/php-mqtt/client/tree/v2.2.0"
},
"install-path": "../php-mqtt/client"
},
{
"name": "psr/log",
"version": "3.0.2",
"version_normalized": "3.0.2.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"time": "2024-09-11T13:17:53+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.2"
},
"install-path": "../psr/log"
}
],
"dev": true,
"dev-package-names": []
}

50
api/vendor/composer/installed.php vendored Normal file
View File

@ -0,0 +1,50 @@
<?php return array(
'root' => array(
'name' => 'root/api',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'myclabs/php-enum' => array(
'pretty_version' => '1.8.5',
'version' => '1.8.5.0',
'reference' => 'e7be26966b7398204a234f8673fdad5ac6277802',
'type' => 'library',
'install_path' => __DIR__ . '/../myclabs/php-enum',
'aliases' => array(),
'dev_requirement' => false,
),
'php-mqtt/client' => array(
'pretty_version' => 'v2.2.0',
'version' => '2.2.0.0',
'reference' => '8042ad93e72da8666e27168dc90670e45bdea274',
'type' => 'library',
'install_path' => __DIR__ . '/../php-mqtt/client',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/log' => array(
'pretty_version' => '3.0.2',
'version' => '3.0.2.0',
'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/log',
'aliases' => array(),
'dev_requirement' => false,
),
'root/api' => array(
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

26
api/vendor/composer/platform_check.php vendored Normal file
View File

@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 80000)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.0.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

18
api/vendor/myclabs/php-enum/LICENSE vendored Normal file
View File

@ -0,0 +1,18 @@
The MIT License (MIT)
Copyright (c) 2015 My C-Labs
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

196
api/vendor/myclabs/php-enum/README.md vendored Normal file
View File

@ -0,0 +1,196 @@
# PHP Enum implementation inspired from SplEnum
[![GitHub Actions][GA Image]][GA Link]
[![Latest Stable Version](https://poser.pugx.org/myclabs/php-enum/version.png)](https://packagist.org/packages/myclabs/php-enum)
[![Total Downloads](https://poser.pugx.org/myclabs/php-enum/downloads.png)](https://packagist.org/packages/myclabs/php-enum)
[![Psalm Shepherd][Shepherd Image]][Shepherd Link]
Maintenance for this project is [supported via Tidelift](https://tidelift.com/subscription/pkg/packagist-myclabs-php-enum?utm_source=packagist-myclabs-php-enum&utm_medium=referral&utm_campaign=readme).
## Why?
First, and mainly, `SplEnum` is not integrated to PHP, you have to install the extension separately.
Using an enum instead of class constants provides the following advantages:
- You can use an enum as a parameter type: `function setAction(Action $action) {`
- You can use an enum as a return type: `function getAction() : Action {`
- You can enrich the enum with methods (e.g. `format`, `parse`, …)
- You can extend the enum to add new values (make your enum `final` to prevent it)
- You can get a list of all the possible values (see below)
This Enum class is not intended to replace class constants, but only to be used when it makes sense.
## Installation
```
composer require myclabs/php-enum
```
## Declaration
```php
use MyCLabs\Enum\Enum;
/**
* Action enum
*
* @extends Enum<Action::*>
*/
final class Action extends Enum
{
private const VIEW = 'view';
private const EDIT = 'edit';
}
```
## Usage
```php
$action = Action::VIEW();
// or with a dynamic key:
$action = Action::$key();
// or with a dynamic value:
$action = Action::from($value);
// or
$action = new Action($value);
```
As you can see, static methods are automatically implemented to provide quick access to an enum value.
One advantage over using class constants is to be able to use an enum as a parameter type:
```php
function setAction(Action $action) {
// ...
}
```
## Documentation
- `__construct()` The constructor checks that the value exist in the enum
- `__toString()` You can `echo $myValue`, it will display the enum value (value of the constant)
- `getValue()` Returns the current value of the enum
- `getKey()` Returns the key of the current value on Enum
- `equals()` Tests whether enum instances are equal (returns `true` if enum values are equal, `false` otherwise)
Static methods:
- `from()` Creates an Enum instance, checking that the value exist in the enum
- `toArray()` method Returns all possible values as an array (constant name in key, constant value in value)
- `keys()` Returns the names (keys) of all constants in the Enum class
- `values()` Returns instances of the Enum class of all Enum constants (constant name in key, Enum instance in value)
- `isValid()` Check if tested value is valid on enum set
- `isValidKey()` Check if tested key is valid on enum set
- `assertValidValue()` Assert the value is valid on enum set, throwing exception otherwise
- `search()` Return key for searched value
### Static methods
```php
final class Action extends Enum
{
private const VIEW = 'view';
private const EDIT = 'edit';
}
// Static method:
$action = Action::VIEW();
$action = Action::EDIT();
```
Static method helpers are implemented using [`__callStatic()`](http://www.php.net/manual/en/language.oop5.overloading.php#object.callstatic).
If you care about IDE autocompletion, you can either implement the static methods yourself:
```php
final class Action extends Enum
{
private const VIEW = 'view';
/**
* @return Action
*/
public static function VIEW() {
return new Action(self::VIEW);
}
}
```
or you can use phpdoc (this is supported in PhpStorm for example):
```php
/**
* @method static Action VIEW()
* @method static Action EDIT()
*/
final class Action extends Enum
{
private const VIEW = 'view';
private const EDIT = 'edit';
}
```
## Native enums and migration
Native enum arrived to PHP in version 8.1: https://www.php.net/enumerations
If your project is running PHP 8.1+ or your library has it as a minimum requirement you should use it instead of this library.
When migrating from `myclabs/php-enum`, the effort should be small if the usage was in the recommended way:
- private constants
- final classes
- no method overridden
Changes for migration:
- Class definition should be changed from
```php
/**
* @method static Action VIEW()
* @method static Action EDIT()
*/
final class Action extends Enum
{
private const VIEW = 'view';
private const EDIT = 'edit';
}
```
to
```php
enum Action: string
{
case VIEW = 'view';
case EDIT = 'edit';
}
```
All places where the class was used as a type will continue to work.
Usages and the change needed:
| Operation | myclabs/php-enum | native enum |
|----------------------------------------------------------------|----------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Obtain an instance will change from | `$enumCase = Action::VIEW()` | `$enumCase = Action::VIEW` |
| Create an enum from a backed value | `$enumCase = new Action('view')` | `$enumCase = Action::from('view')` |
| Get the backed value of the enum instance | `$enumCase->getValue()` | `$enumCase->value` |
| Compare two enum instances | `$enumCase1 == $enumCase2` <br/> or <br/> `$enumCase1->equals($enumCase2)` | `$enumCase1 === $enumCase2` |
| Get the key/name of the enum instance | `$enumCase->getKey()` | `$enumCase->name` |
| Get a list of all the possible instances of the enum | `Action::values()` | `Action::cases()` |
| Get a map of possible instances of the enum mapped by name | `Action::values()` | `array_combine(array_map(fn($case) => $case->name, Action::cases()), Action::cases())` <br/> or <br/> `(new ReflectionEnum(Action::class))->getConstants()` |
| Get a list of all possible names of the enum | `Action::keys()` | `array_map(fn($case) => $case->name, Action::cases())` |
| Get a list of all possible backed values of the enum | `Action::toArray()` | `array_map(fn($case) => $case->value, Action::cases())` |
| Get a map of possible backed values of the enum mapped by name | `Action::toArray()` | `array_combine(array_map(fn($case) => $case->name, Action::cases()), array_map(fn($case) => $case->value, Action::cases()))` <br/> or <br/> `array_map(fn($case) => $case->value, (new ReflectionEnum(Action::class))->getConstants()))` |
## Related projects
- [PHP 8.1+ native enum](https://www.php.net/enumerations)
- [Doctrine enum mapping](https://github.com/acelaya/doctrine-enum-type)
- [Symfony ParamConverter integration](https://github.com/Ex3v/MyCLabsEnumParamConverter)
- [PHPStan integration](https://github.com/timeweb/phpstan-enum)
[GA Image]: https://github.com/myclabs/php-enum/workflows/CI/badge.svg
[GA Link]: https://github.com/myclabs/php-enum/actions?query=workflow%3A%22CI%22+branch%3Amaster
[Shepherd Image]: https://shepherd.dev/github/myclabs/php-enum/coverage.svg
[Shepherd Link]: https://shepherd.dev/github/myclabs/php-enum

11
api/vendor/myclabs/php-enum/SECURITY.md vendored Normal file
View File

@ -0,0 +1,11 @@
# Security Policy
## Supported Versions
Only the latest stable release is supported.
## Reporting a Vulnerability
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security).
Tidelift will coordinate the fix and disclosure.

View File

@ -0,0 +1,36 @@
{
"name": "myclabs/php-enum",
"type": "library",
"description": "PHP Enum implementation",
"keywords": ["enum"],
"homepage": "https://github.com/myclabs/php-enum",
"license": "MIT",
"authors": [
{
"name": "PHP Enum contributors",
"homepage": "https://github.com/myclabs/php-enum/graphs/contributors"
}
],
"autoload": {
"psr-4": {
"MyCLabs\\Enum\\": "src/"
},
"classmap": [
"stubs/Stringable.php"
]
},
"autoload-dev": {
"psr-4": {
"MyCLabs\\Tests\\Enum\\": "tests/"
}
},
"require": {
"php": "^7.3 || ^8.0",
"ext-json": "*"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "1.*",
"vimeo/psalm": "^4.6.2 || ^5.2"
}
}

319
api/vendor/myclabs/php-enum/src/Enum.php vendored Normal file
View File

@ -0,0 +1,319 @@
<?php
/**
* @link http://github.com/myclabs/php-enum
* @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file)
*/
namespace MyCLabs\Enum;
/**
* Base Enum class
*
* Create an enum by implementing this class and adding class constants.
*
* @author Matthieu Napoli <matthieu@mnapoli.fr>
* @author Daniel Costa <danielcosta@gmail.com>
* @author Mirosław Filip <mirfilip@gmail.com>
*
* @psalm-template T
* @psalm-immutable
* @psalm-consistent-constructor
*/
abstract class Enum implements \JsonSerializable, \Stringable
{
/**
* Enum value
*
* @var mixed
* @psalm-var T
*/
protected $value;
/**
* Enum key, the constant name
*
* @var string
*/
private $key;
/**
* Store existing constants in a static cache per object.
*
*
* @var array
* @psalm-var array<class-string, array<string, mixed>>
*/
protected static $cache = [];
/**
* Cache of instances of the Enum class
*
* @var array
* @psalm-var array<class-string, array<string, static>>
*/
protected static $instances = [];
/**
* Creates a new value of some type
*
* @psalm-pure
* @param mixed $value
*
* @psalm-param T $value
* @throws \UnexpectedValueException if incompatible type is given.
*/
public function __construct($value)
{
if ($value instanceof static) {
/** @psalm-var T */
$value = $value->getValue();
}
/** @psalm-suppress ImplicitToStringCast assertValidValueReturningKey returns always a string but psalm has currently an issue here */
$this->key = static::assertValidValueReturningKey($value);
/** @psalm-var T */
$this->value = $value;
}
/**
* This method exists only for the compatibility reason when deserializing a previously serialized version
* that didn't had the key property
*/
public function __wakeup()
{
/** @psalm-suppress DocblockTypeContradiction key can be null when deserializing an enum without the key */
if ($this->key === null) {
/**
* @psalm-suppress InaccessibleProperty key is not readonly as marked by psalm
* @psalm-suppress PossiblyFalsePropertyAssignmentValue deserializing a case that was removed
*/
$this->key = static::search($this->value);
}
}
/**
* @param mixed $value
* @return static
*/
public static function from($value): self
{
$key = static::assertValidValueReturningKey($value);
return self::__callStatic($key, []);
}
/**
* @psalm-pure
* @return mixed
* @psalm-return T
*/
public function getValue()
{
return $this->value;
}
/**
* Returns the enum key (i.e. the constant name).
*
* @psalm-pure
* @return string
*/
public function getKey()
{
return $this->key;
}
/**
* @psalm-pure
* @psalm-suppress InvalidCast
* @return string
*/
public function __toString()
{
return (string)$this->value;
}
/**
* Determines if Enum should be considered equal with the variable passed as a parameter.
* Returns false if an argument is an object of different class or not an object.
*
* This method is final, for more information read https://github.com/myclabs/php-enum/issues/4
*
* @psalm-pure
* @psalm-param mixed $variable
* @return bool
*/
final public function equals($variable = null): bool
{
return $variable instanceof self
&& $this->getValue() === $variable->getValue()
&& static::class === \get_class($variable);
}
/**
* Returns the names (keys) of all constants in the Enum class
*
* @psalm-pure
* @psalm-return list<string>
* @return array
*/
public static function keys()
{
return \array_keys(static::toArray());
}
/**
* Returns instances of the Enum class of all Enum constants
*
* @psalm-pure
* @psalm-return array<string, static>
* @return static[] Constant name in key, Enum instance in value
*/
public static function values()
{
$values = array();
/** @psalm-var T $value */
foreach (static::toArray() as $key => $value) {
/** @psalm-suppress UnsafeGenericInstantiation */
$values[$key] = new static($value);
}
return $values;
}
/**
* Returns all possible values as an array
*
* @psalm-pure
* @psalm-suppress ImpureStaticProperty
*
* @psalm-return array<string, mixed>
* @return array Constant name in key, constant value in value
*/
public static function toArray()
{
$class = static::class;
if (!isset(static::$cache[$class])) {
/** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */
$reflection = new \ReflectionClass($class);
/** @psalm-suppress ImpureMethodCall this reflection API usage has no side-effects here */
static::$cache[$class] = $reflection->getConstants();
}
return static::$cache[$class];
}
/**
* Check if is valid enum value
*
* @param $value
* @psalm-param mixed $value
* @psalm-pure
* @psalm-assert-if-true T $value
* @return bool
*/
public static function isValid($value)
{
return \in_array($value, static::toArray(), true);
}
/**
* Asserts valid enum value
*
* @psalm-pure
* @psalm-assert T $value
* @param mixed $value
*/
public static function assertValidValue($value): void
{
self::assertValidValueReturningKey($value);
}
/**
* Asserts valid enum value
*
* @psalm-pure
* @psalm-assert T $value
* @param mixed $value
* @return string
*/
private static function assertValidValueReturningKey($value): string
{
if (false === ($key = static::search($value))) {
throw new \UnexpectedValueException("Value '$value' is not part of the enum " . static::class);
}
return $key;
}
/**
* Check if is valid enum key
*
* @param $key
* @psalm-param string $key
* @psalm-pure
* @return bool
*/
public static function isValidKey($key)
{
$array = static::toArray();
return isset($array[$key]) || \array_key_exists($key, $array);
}
/**
* Return key for value
*
* @param mixed $value
*
* @psalm-param mixed $value
* @psalm-pure
* @return string|false
*/
public static function search($value)
{
return \array_search($value, static::toArray(), true);
}
/**
* Returns a value when called statically like so: MyEnum::SOME_VALUE() given SOME_VALUE is a class constant
*
* @param string $name
* @param array $arguments
*
* @return static
* @throws \BadMethodCallException
*
* @psalm-pure
*/
public static function __callStatic($name, $arguments)
{
$class = static::class;
if (!isset(self::$instances[$class][$name])) {
$array = static::toArray();
if (!isset($array[$name]) && !\array_key_exists($name, $array)) {
$message = "No static method or enum constant '$name' in class " . static::class;
throw new \BadMethodCallException($message);
}
/** @psalm-suppress UnsafeGenericInstantiation */
return self::$instances[$class][$name] = new static($array[$name]);
}
return clone self::$instances[$class][$name];
}
/**
* Specify data which should be serialized to JSON. This method returns data that can be serialized by json_encode()
* natively.
*
* @return mixed
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->getValue();
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace MyCLabs\Enum\PHPUnit;
use MyCLabs\Enum\Enum;
use SebastianBergmann\Comparator\ComparisonFailure;
/**
* Use this Comparator to get nice output when using PHPUnit assertEquals() with Enums.
*
* Add this to your PHPUnit bootstrap PHP file:
*
* \SebastianBergmann\Comparator\Factory::getInstance()->register(new \MyCLabs\Enum\PHPUnit\Comparator());
*/
final class Comparator extends \SebastianBergmann\Comparator\Comparator
{
public function accepts($expected, $actual)
{
return $expected instanceof Enum && (
$actual instanceof Enum || $actual === null
);
}
/**
* @param Enum $expected
* @param Enum|null $actual
*
* @return void
*/
public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false)
{
if ($expected->equals($actual)) {
return;
}
throw new ComparisonFailure(
$expected,
$actual,
$this->formatEnum($expected),
$this->formatEnum($actual),
false,
'Failed asserting that two Enums are equal.'
);
}
private function formatEnum(?Enum $enum = null)
{
if ($enum === null) {
return "null";
}
return get_class($enum)."::{$enum->getKey()}()";
}
}

View File

@ -0,0 +1,11 @@
<?php
if (\PHP_VERSION_ID < 80000 && !interface_exists('Stringable')) {
interface Stringable
{
/**
* @return string
*/
public function __toString();
}
}

2503
api/vendor/php-mqtt/client/.ci/emqx.conf vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
<?xml version="1.0"?>
<hivemq xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/peez80/docker-hivemq/master/hivemq-config.xsd"
>
<listeners>
<!-- MQTT port without TLS -->
<tcp-listener>
<port>1883</port>
<bind-address>0.0.0.0</bind-address>
</tcp-listener>
<!-- MQTT port with TLS but without client certificate validation -->
<tls-tcp-listener>
<port>8883</port>
<bind-address>0.0.0.0</bind-address>
<tls>
<keystore>
<path>/hivemq-certs/server.jks</path>
<password>s3cr3t</password>
<private-key-password>s3cr3t</private-key-password>
</keystore>
<protocols>
<protocol>TLSv1.3</protocol>
<protocol>TLSv1.2</protocol>
<protocol>TLSv1.1</protocol>
<protocol>TLSv1</protocol>
</protocols>
</tls>
</tls-tcp-listener>
<!-- MQTT port with TLS and with client certificate validation -->
<tls-tcp-listener>
<port>8884</port>
<bind-address>0.0.0.0</bind-address>
<tls>
<client-authentication-mode>REQUIRED</client-authentication-mode>
<truststore>
<path>/hivemq-certs/ca.jks</path>
<password>s3cr3t</password>
</truststore>
<keystore>
<path>/hivemq-certs/server.jks</path>
<password>s3cr3t</password>
<private-key-password>s3cr3t</private-key-password>
</keystore>
<protocols>
<protocol>TLSv1.3</protocol>
<protocol>TLSv1.2</protocol>
<protocol>TLSv1.1</protocol>
<protocol>TLSv1</protocol>
</protocols>
</tls>
</tls-tcp-listener>
</listeners>
</hivemq>

View File

@ -0,0 +1,31 @@
# Config file for mosquitto
per_listener_settings true
# Port to use for the default listener.
listener 1883
allow_anonymous true
# Port to use for the default listener with authentication.
listener 1884
password_file /mosquitto/config/mosquitto.passwd
allow_anonymous false
# =================================================================
# Extra listeners
# =================================================================
# TLS listener without client certificate requirement
listener 8883
cafile /mosquitto-certs/ca.crt
certfile /mosquitto-certs/server.crt
keyfile /mosquitto-certs/server.key
require_certificate false
allow_anonymous true
# TLS listener with client certificate requirement
listener 8884
cafile /mosquitto-certs/ca.crt
certfile /mosquitto-certs/server.crt
keyfile /mosquitto-certs/server.key
require_certificate true
allow_anonymous true

View File

@ -0,0 +1 @@
ci-test-user:$6$QypQBNSQKE5bg6Ec$nzACfxhQ9qiYFByPPM/6GP/9kOWwDzEftN0EJPkS6M0PWqL55jAbBxUO863oWwhJ2q/YaubfLbe3xwwhBuoStQ==

View File

@ -0,0 +1,11 @@
listeners.tcp.default = 5672
loopback_users.guest = false
mqtt.listeners.tcp.default = 1883
mqtt.listeners.ssl = none
mqtt.allow_anonymous = true
mqtt.default_user = guest
mqtt.default_pass = guest
mqtt.vhost = /
mqtt.exchange = amq.topic
mqtt.subscription_ttl = 1800000

View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1,23 @@
version: 2
updates:
- package-ecosystem: "composer"
directory: "/"
allow:
- dependency-type: "development"
schedule:
interval: "daily"
time: "05:00"
timezone: "Europe/Vienna"
labels:
- "composer dependencies"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "05:00"
timezone: "Europe/Vienna"
labels:
- "github actions"

View File

@ -0,0 +1,25 @@
changelog:
exclude:
labels:
- ignore-for-release
authors:
- octocat
categories:
- title: Added
labels:
- enhancement
- title: Deprecated
labels:
- deprecated
- title: Removed
labels:
- removed
- title: Fixed
labels:
- bug
- title: Security
labels:
- security
- title: Changed
labels:
- "*"

View File

@ -0,0 +1,23 @@
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
pull-requests: write
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v4
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4
with:
comment-summary-in-pr: true
fail-on-scopes: 'runtime, development, unknown'
fail-on-severity: 'low'
license-check: true
vulnerability-check: true

View File

@ -0,0 +1,140 @@
name: Tests
on:
push:
branches:
- master
pull_request_target:
types: [opened, synchronize, reopened]
jobs:
test-all:
name: Test PHP ${{ matrix.php-version }} using broker [${{ matrix.mqtt-broker }}]
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.1', '8.2', '8.3', '8.4']
mqtt-broker: ['mosquitto-1.6', 'mosquitto-2.0', 'hivemq', 'emqx', 'rabbitmq']
include:
- php-version: '8.4'
mqtt-broker: 'mosquitto-2.0'
run-sonarqube-analysis: true
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: phpunit:9.5.0
coverage: pcov
- name: Setup problem matchers for PHP
run: echo "::add-matcher::${{ runner.tool_cache }}/php.json"
- name: Setup problem matchers for PHPUnit
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install Composer dependencies
run: composer install --prefer-dist
- name: Generate certificates for tests
run: |
sh create-certificates.sh
chmod u+rx,g+rx ${{ github.workspace }}/.ci/tls
chmod a+r ${{ github.workspace }}/.ci/tls/*
- name: Start Mosquitto 1.6 message broker
if: matrix.mqtt-broker == 'mosquitto-1.6'
uses: Namoshek/mosquitto-github-action@v1
with:
version: '1.6'
ports: '1883:1883 1884:1884 8883:8883 8884:8884'
certificates: ${{ github.workspace }}/.ci/tls
config: ${{ github.workspace }}/.ci/mosquitto.conf
password-file: ${{ github.workspace}}/.ci/mosquitto.passwd
- name: Start Mosquitto 2.0 message broker
if: matrix.mqtt-broker == 'mosquitto-2.0'
uses: Namoshek/mosquitto-github-action@v1
with:
version: '2.0'
ports: '1883:1883 1884:1884 8883:8883 8884:8884'
certificates: ${{ github.workspace }}/.ci/tls
config: ${{ github.workspace }}/.ci/mosquitto.conf
password-file: ${{ github.workspace}}/.ci/mosquitto.passwd
- name: Start HiveMQ message broker
if: matrix.mqtt-broker == 'hivemq'
uses: Namoshek/hivemq4-github-action@v1
with:
version: '4.8.5'
ports: '1883:1883 8883:8883 8884:8884'
certificates: ${{ github.workspace }}/.ci/tls
config: ${{ github.workspace }}/.ci/hivemq.xml
- name: Start EMQ X message broker
if: matrix.mqtt-broker == 'emqx'
uses: Namoshek/emqx-github-action@v1.0.2
with:
version: '4.4.3'
ports: '1883:1883'
config: ${{ github.workspace }}/.ci/emqx.conf
- name: Start RabbitMQ message broker
if: matrix.mqtt-broker == 'rabbitmq'
uses: namoshek/rabbitmq-github-action@v1.1.0
with:
version: '3.8.9'
ports: '1883:1883'
config: ${{ github.workspace }}/.ci/rabbitmq.conf
plugins: 'rabbitmq_mqtt'
- name: Wait a bit until MQTT broker has started
run: sleep 45
- name: Run phpunit tests
run: composer test
env:
MQTT_BROKER_HOST: 'localhost'
MQTT_BROKER_PORT: 1883
MQTT_BROKER_PORT_WITH_AUTHENTICATION: ${{ (matrix.mqtt-broker == 'mosquitto-1.6' || matrix.mqtt-broker == 'mosquitto-2.0') && 1884 || 1883 }}
MQTT_BROKER_TLS_PORT: 8883
MQTT_BROKER_TLS_WITH_CLIENT_CERT_PORT: 8884
MQTT_BROKER_USERNAME: ${{ (matrix.mqtt-broker == 'mosquitto-1.6' || matrix.mqtt-broker == 'mosquitto-2.0') && 'ci-test-user' || '' }}
MQTT_BROKER_PASSWORD: ${{ (matrix.mqtt-broker == 'mosquitto-1.6' || matrix.mqtt-broker == 'mosquitto-2.0') && secrets.CI_MOSQUITTO_CI_TEST_USER_PASSWORD || '' }}
SKIP_TLS_TESTS: ${{ matrix.mqtt-broker == 'emqx' || matrix.mqtt-broker == 'rabbitmq' }}
- name: Dump Docker logs on failure
if: failure()
uses: jwalton/gh-docker-logs@v2
- name: Prepare paths for SonarQube analysis
if: matrix.run-sonarqube-analysis
run: |
sed -i "s|$GITHUB_WORKSPACE|/github/workspace|g" phpunit.coverage-clover.xml
sed -i "s|$GITHUB_WORKSPACE|/github/workspace|g" phpunit.report-junit.xml
- name: Run SonarQube analysis
uses: sonarsource/sonarcloud-github-action@v3.1.0
if: matrix.run-sonarqube-analysis
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }}

6
api/vendor/php-mqtt/client/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.idea/
.phpunit.result.cache
composer.lock
phpunit.coverage*.xml
phpunit.report*.xml
/vendor/

88
api/vendor/php-mqtt/client/.phpcs.xml vendored Normal file
View File

@ -0,0 +1,88 @@
<?xml version="1.0"?>
<ruleset name="php-mqtt Code Style Standard">
<description>php-mqtt Code Style Standard</description>
<rule ref="PSR1"/>
<rule ref="PSR2">
<exclude name="PSR2.Methods.MethodDeclaration.AbstractAfterVisibility"/>
<exclude name="Squiz.ControlStructures.ControlSignature.SpaceAfterCloseParenthesis"/>
</rule>
<rule ref="Generic.Arrays.ArrayIndent">
<exclude name="Generic.Arrays.ArrayIndent.CloseBraceNotNewLine"/>
</rule>
<rule ref="Generic.Classes.DuplicateClassName"/>
<rule ref="Generic.CodeAnalysis.EmptyStatement">
<exclude name="Generic.CodeAnalysis.EmptyStatement.DetectedCatch"/>
</rule>
<rule ref="Generic.CodeAnalysis.ForLoopShouldBeWhileLoop"/>
<rule ref="Generic.CodeAnalysis.ForLoopWithTestFunctionCall"/>
<rule ref="Generic.CodeAnalysis.JumbledIncrementer"/>
<rule ref="Generic.CodeAnalysis.UnconditionalIfStatement"/>
<rule ref="Generic.CodeAnalysis.UnnecessaryFinalModifier"/>
<rule ref="Generic.CodeAnalysis.UselessOverridingMethod"/>
<rule ref="Generic.Commenting.Todo">
<exclude-pattern>src/*</exclude-pattern>
</rule>
<rule ref="Generic.ControlStructures.InlineControlStructure"/>
<rule ref="Generic.Files.ByteOrderMark"/>
<rule ref="Generic.Files.LineEndings"/>
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="150"/>
<property name="absoluteLineLimit" value="0"/>
</properties>
</rule>
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
<rule ref="Generic.Formatting.MultipleStatementAlignment"/>
<rule ref="Generic.Formatting.SpaceAfterCast"/>
<rule ref="Generic.Functions.CallTimePassByReference"/>
<rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
<rule ref="Generic.Functions.OpeningFunctionBraceBsdAllman"/>
<rule ref="Generic.Metrics.CyclomaticComplexity">
<properties>
<property name="complexity" value="50"/>
<property name="absoluteComplexity" value="100"/>
</properties>
</rule>
<rule ref="Generic.Metrics.NestingLevel">
<properties>
<property name="nestingLevel" value="10"/>
<property name="absoluteNestingLevel" value="30"/>
</properties>
</rule>
<rule ref="Generic.NamingConventions.ConstructorName"/>
<rule ref="Generic.PHP.LowerCaseConstant"/>
<rule ref="Generic.PHP.DeprecatedFunctions"/>
<rule ref="Generic.PHP.DisallowShortOpenTag"/>
<rule ref="Generic.PHP.ForbiddenFunctions"/>
<rule ref="Generic.WhiteSpace.DisallowTabIndent"/>
<rule ref="Generic.WhiteSpace.ScopeIndent">
<properties>
<property name="indent" value="4"/>
</properties>
</rule>
<rule ref="MySource.PHP.EvalObjectFactory"/>
<rule ref="PEAR.Commenting.ClassComment">
<exclude name="PEAR.Commenting.ClassComment.MissingAuthorTag"/>
<exclude name="PEAR.Commenting.ClassComment.MissingCategoryTag"/>
<exclude name="PEAR.Commenting.ClassComment.MissingLicenseTag"/>
<exclude name="PEAR.Commenting.ClassComment.MissingLinkTag"/>
</rule>
<rule ref="PEAR.Commenting.ClassComment.Missing"/>
<rule ref="PEAR.Commenting.ClassComment.MissingPackageTag"/>
<rule ref="PEAR.Commenting.InlineComment"/>
<rule ref="PSR1.Classes.ClassDeclaration.MissingNamespace"/>
<rule ref="PSR2.Methods.FunctionClosingBrace.SpacingBeforeClose"/>
<rule ref="Squiz.Arrays.ArrayDeclaration.NoCommaAfterLast"/>
<rule ref="Squiz.Functions.MultiLineFunctionDeclaration.NewlineBeforeOpenBrace">
<exclude-pattern>src/*</exclude-pattern>
</rule>
<rule ref="Zend.Files.ClosingTag"/>
<file>src</file>
<arg name="colors"/>
<arg value="sp"/>
<ini name="memory_limit" value="128M"/>
</ruleset>

72
api/vendor/php-mqtt/client/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,72 @@
# Changelog
## Version `v1.0.0`
Significant improvements to the architecture, API and design of the library have been part of `v1.0.0`.
Upgrading should be rather simple for most users though, since the public API has not changed a lot
and only in places which are not used too frequently.
A lot of effort has been put into this summary to document as many changes as possible.
It is impossible to give a guarantee about the completeness of this list though.
You should cover your uses of the library with tests yourself as well.
The following summary compares `v0.3.0` to `v1.0.0`.
### Breaking Changes
- The library does now require PHP 7.4 and supports PHP 8.0. This move was made with the clear intention to drop support for PHP 7.4 at some point.
- The primary interface and class of the library have been renamed to StudlyCaps to follow PSR-2:
- `\PhpMqtt\Client\Contracts\MQTTClient` &rarr; `\PhpMqtt\Client\Contracts\MqttClient`
- `\PhpMqtt\Client\MQTTClient` &rarr; `\PhpMqtt\Client\MqttClient`
- `\PhpMqtt\Client\Exceptions\MQTTClientException` &rarr; `\PhpMqtt\Client\Exceptions\MqttClientException`
- Protocol specific logic has been extracted from the `MQTTClient` class to a new interface `\PhpMqtt\Client\Contracts\MessageProcessor` and the respective implementation for MQTT 3.1, `\PhpMqtt\Client\MessageProcessors\Mqtt31MessageProcessor`.
- The `MessageProcessor` is responsible for parsing and building packages on a byte level.
- Splitting the logic from the main class did not only reduce the overall complexity of the class, it also made testing a lot easier and builds a solid foundation for future development and extension by implementing more protocol versions (like MQTT 5).
- Some `protected` properties have been changed to `private` to ensure they are not manipulated outside the offered scope, which is enforced through getters and setters. This change only affects users which actively inherited their own implementation from the library.
- The QoS 2 message flow is now implemented properly and should just work.
#### Connection Settings
- The `$caFile` parameter of the `MQTTClient` constructor as well as the `$username` and `$password` parameters of the `MQTTClient::connect()` method have been moved to the `ConnectionSettings` class.
- The `ConnectionSettings` use fluent setters for configuration now ([see README](README.md)).
- The `ConnectionSettings`` passed to `MQTTClient::connect()` are now validated and may not contain invalid configuration. In case of invalid configuration, a `\PhpMqtt\Client\Exceptions\ConfigurationInvalidException` is thrown.
- Additional TLS options have been added to the `ConnectionSettings` to support more uses cases with secured connections.
#### Methods
- Most methods can now throw a `\PhpMqtt\Client\Exceptions\RepositoryException` if an interaction with the repository fails. This should happen with the `MemoryRepository` only in exceptional situations, but when implementing persisted repositories, this may happen more frequently and should therefore be considered.
- The `MQTTClient::connect()` method had a parameter called `$sendCleanSessionFlag` while the `MqttClient::connect()` method has the same parameter, but called `$useCleanSession`. The parameters `$username` and `$password` have been removed entirely and are now part of the `ConnectionSettings`.
- The method `MQTTClient::close()` has been renamed to `MqttClient::disconnect()`.
- The parameter `$topic` of `MQTTClient::subscribe()` has been renamed to `$topicFilter` to reflect its meaning (which is a topic, but with wildcards). The `$callback` parameter can be `null` now and has `null` as default.
- The parameter `$topic` of `MQTTClient::unsubscribe()` has been renamed to `$topicFilter` as well.
#### Exceptions
- New exceptions have been introduced and old ones were removed. All exceptions inherit from `\PhpMqtt\Client\Exceptions\MqttClientException` as base. You should ensure your calls to methods of the `MqttClient` handle the exceptions appropriately.
- The exception constants previously defined on the `\PhpMqtt\Client\MQTTClient` class have been moved to the respective exception classes. This change only affects you if you used these constants to render detailed exception information for your users.
#### Repositories
- The `\PhpMqtt\Client\Contracts\Repository` interface has been changed significantly and summarizing all changes would be quite hard anyway. We therefore encourage you to have a look at the interface again and update your own implementation(s) of it, if you have any.
#### Logger
- The `\PhpMqtt\Client\Logger` implementation of `Psr\Log\LoggerInterface` does now decorate the log output with details about the MQTT client (format: `MQTT [{host}:{port}] [{clientId}] {message}`).
### Additions
- It is now possible to register event handlers for received messages. In combination with subscriptions without a callback, this allows to use centralized logic for multiple subscriptions. It also can be used for centralized logging, for example.
- A lot of unit and integration tests have been added which cover most parts of the library, especially the non-exception paths.
- All unit tests, integration tests, and the code style are enforced using a GitHub Actions workflow which runs under Ubuntu against multiple MQTT brokers (currently Mosquitto, HiveMQ and EMQ X). Contributing became easier therefore, but we expect that tests are added for changes and additions.
- To run the tests locally, an MQTT broker without authorization needs to run at `localhost:1883` (or the configuration in `phpunit.xml` is changed instead).
- The project is now analyzed using [sonarcloud.io](https://sonarcloud.io/dashboard?id=php-mqtt_client) which helps us keep up the high standards of the library.
#### Methods
- `MqttClient::isConnected()`: returns `true` if a connection is established (socket opened), and `false` otherwise.
- `MqttClient::getReceivedBytes()`: returns the number of raw bytes received from the broker (this includes meta information and not only message contents).
- `MqttClient::getSentBytes()`: returns the number of raw bytes sent to the broker (this includes meta information and not only message contents).
### Removals
_No functionality has been removed in this version._

21
api/vendor/php-mqtt/client/LICENSE.md vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Marvin Mall <marvin-mall@msn.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

319
api/vendor/php-mqtt/client/README.md vendored Normal file
View File

@ -0,0 +1,319 @@
# php-mqtt/client
[![Latest Stable Version](https://poser.pugx.org/php-mqtt/client/v)](https://packagist.org/packages/php-mqtt/client)
[![Total Downloads](https://poser.pugx.org/php-mqtt/client/downloads)](https://packagist.org/packages/php-mqtt/client)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=coverage)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=alert_status)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=security_rating)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=php-mqtt_client&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=php-mqtt_client)
[![License](https://poser.pugx.org/php-mqtt/client/license)](https://packagist.org/packages/php-mqtt/client)
[`php-mqtt/client`](https://packagist.org/packages/php-mqtt/client) was created by, and is maintained
by [Marvin Mall](https://github.com/namoshek).
It allows you to connect to an MQTT broker where you can publish messages and subscribe to topics.
The current implementation supports all QoS levels ([with limitations](#limitations)).
## Installation
The package is available on [packagist.org](https://packagist.org/packages/php-mqtt/client) and can be installed using `composer`:
```bash
composer require php-mqtt/client
```
The package requires PHP version 8.0 or higher.
## Usage
In the following, only a few very basic examples are given. For more elaborate examples, have a look at the
[`php-mqtt/client-examples` repository](https://github.com/php-mqtt/client-examples).
### Publish
A very basic publish example using QoS 0 requires only three steps: connect, publish and disconnect
```php
$server = 'some-broker.example.com';
$port = 1883;
$clientId = 'test-publisher';
$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId);
$mqtt->connect();
$mqtt->publish('php-mqtt/client/test', 'Hello World!', 0);
$mqtt->disconnect();
```
If you do not want to pass a `$clientId`, a random one will be generated for you. This will basically force a clean session implicitly.
Be also aware that most of the methods can throw exceptions. The above example does not add any exception handling for brevity.
### Subscribe
Subscribing is a little more complex than publishing as it requires to run an event loop which reads, parses and handles messages from the broker:
```php
$server = 'some-broker.example.com';
$port = 1883;
$clientId = 'test-subscriber';
$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId);
$mqtt->connect();
$mqtt->subscribe('php-mqtt/client/test', function ($topic, $message, $retained, $matchedWildcards) {
echo sprintf("Received message on topic [%s]: %s\n", $topic, $message);
}, 0);
$mqtt->loop(true);
$mqtt->disconnect();
```
While the loop is active, you can use `$mqtt->interrupt()` to send an interrupt signal to the loop.
This will terminate the loop before it starts its next iteration. You can call this method using `pcntl_signal(SIGINT, $handler)` for example:
```php
pcntl_async_signals(true);
$clientId = 'test-subscriber';
$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId);
pcntl_signal(SIGINT, function (int $signal, $info) use ($mqtt) {
$mqtt->interrupt();
});
$mqtt->connect();
$mqtt->subscribe('php-mqtt/client/test', function ($topic, $message, $retained, $matchedWildcards) {
echo sprintf("Received message on topic [%s]: %s\n", $topic, $message);
}, 0);
$mqtt->loop(true);
$mqtt->disconnect();
```
### Client Settings
As shown in the examples above, the `MqttClient` takes the server, port and client id as first, second and third parameter.
As fourth parameter, the protocol level can be passed. Currently supported is MQTT v3.1,
available as constant `MqttClient::MQTT_3_1`.
A fifth parameter allows passing a repository (currently, only a `MemoryRepository` is available by default).
Lastly, a logger can be passed as sixth parameter. If none is given, a null logger is used instead.
Example:
```php
$mqtt = new \PhpMqtt\Client\MqttClient(
$server,
$port,
$clientId,
\PhpMqtt\Client\MqttClient::MQTT_3_1,
new \PhpMqtt\Client\Repositories\MemoryRepository(),
new Logger()
);
```
The `Logger` must implement the `Psr\Log\LoggerInterface`.
### Connection Settings
The `connect()` method of the `MqttClient` takes two optional parameters:
1. A `ConnectionSettings` instance
2. A `boolean` flag indicating whether a clean session should be requested (a random client id does this implicitly)
Example:
```php
$mqtt = new \PhpMqtt\Client\MqttClient($server, $port, $clientId);
$connectionSettings = (new \PhpMqtt\Client\ConnectionSettings)
->setConnectTimeout(3)
->setUseTls(true)
->setTlsSelfSignedAllowed(true);
$mqtt->connect($connectionSettings, true);
```
The `ConnectionSettings` class provides a few settings through a fluent interface. The type itself is immutable,
and a new `ConnectionSettings` instance will be created for each added option.
This also prevents changes to the connection settings after a connection has been established.
The following is a complete list of options with their respective default:
```php
$connectionSettings = (new \PhpMqtt\Client\ConnectionSettings)
// The username used for authentication when connecting to the broker.
->setUsername(null)
// The password used for authentication when connecting to the broker.
->setPassword(null)
// Whether to use a blocking socket when publishing messages or not.
// Normally, this setting can be ignored. When publishing large messages with multiple kilobytes in size,
// a blocking socket may be required if the receipt buffer of the broker is not large enough.
//
// Note: This setting has no effect on subscriptions, only on the publishing of messages.
->useBlockingSocket(false)
// The connect timeout defines the maximum amount of seconds the client will try to establish
// a socket connection with the broker. The value cannot be less than 1 second.
->setConnectTimeout(60)
// The socket timeout is the maximum amount of idle time in seconds for the socket connection.
// If no data is read or sent for the given amount of seconds, the socket will be closed.
// The value cannot be less than 1 second.
->setSocketTimeout(5)
// The resend timeout is the number of seconds the client will wait before sending a duplicate
// of pending messages without acknowledgement. The value cannot be less than 1 second.
->setResendTimeout(10)
// This flag determines whether the client will try to reconnect automatically
// if it notices a disconnect while sending data.
// The setting cannot be used together with the clean session flag.
->setReconnectAutomatically(false)
// Defines the maximum number of reconnect attempts until the client gives up.
// This setting is only relevant if setReconnectAutomatically() is set to true.
->setMaxReconnectAttempts(3)
// Defines the delay between reconnect attempts in milliseconds.
// This setting is only relevant if setReconnectAutomatically() is set to true.
->setDelayBetweenReconnectAttempts(0)
// The keep alive interval is the number of seconds the client will wait without sending a message
// until it sends a keep alive signal (ping) to the broker. The value cannot be less than 1 second
// and may not be higher than 65535 seconds. A reasonable value is 10 seconds (the default).
->setKeepAliveInterval(10)
// If the broker should publish a last will message in the name of the client when the client
// disconnects abruptly, this setting defines the topic on which the message will be published.
//
// A last will message will only be published if both this setting as well as the last will
// message are configured.
->setLastWillTopic(null)
// If the broker should publish a last will message in the name of the client when the client
// disconnects abruptly, this setting defines the message which will be published.
//
// A last will message will only be published if both this setting as well as the last will
// topic are configured.
->setLastWillMessage(null)
// The quality of service level the last will message of the client will be published with,
// if it gets triggered.
->setLastWillQualityOfService(0)
// This flag determines if the last will message of the client will be retained, if it gets
// triggered. Using this setting can be handy to signal that a client is offline by publishing
// a retained offline state in the last will and an online state as first message on connect.
->setRetainLastWill(false)
// This flag determines if TLS should be used for the connection. The port which is used to
// connect to the broker must support TLS connections.
->setUseTls(false)
// This flag determines if the peer certificate is verified, if TLS is used.
->setTlsVerifyPeer(true)
// This flag determines if the peer name is verified, if TLS is used.
->setTlsVerifyPeerName(true)
// This flag determines if self signed certificates of the peer should be accepted.
// Setting this to TRUE implies a security risk and should be avoided for production
// scenarios and public services.
->setTlsSelfSignedAllowed(false)
// The path to a Certificate Authority certificate which is used to verify the peer
// certificate, if TLS is used.
->setTlsCertificateAuthorityFile(null)
// The path to a directory containing Certificate Authority certificates which are
// used to verify the peer certificate, if TLS is used.
->setTlsCertificateAuthorityPath(null)
// The path to a client certificate file used for authentication, if TLS is used.
//
// The client certificate must be PEM encoded. It may optionally contain the
// certificate chain of issuers.
->setTlsClientCertificateFile(null)
// The path to a client certificate key file used for authentication, if TLS is used.
//
// This option requires ConnectionSettings::setTlsClientCertificateFile() to be used as well.
->setTlsClientCertificateKeyFile(null)
// The passphrase used to decrypt the private key of the client certificate,
// which in return is used for authentication, if TLS is used.
//
// This option requires ConnectionSettings::setTlsClientCertificateFile() and
// ConnectionSettings::setTlsClientCertificateKeyFile() to be used as well.
->setTlsClientCertificateKeyPassphrase(null);
// The TLS ALPN is used to establish a TLS encrypted mqtt connection on port 443,
// which usually is reserved for TLS encrypted HTTP traffic.
->setTlsAlpn(null);
```
## Features
- Supported MQTT Versions
- [x] v3 (just don't use v3.1 features like username & password)
- [x] v3.1
- [x] v3.1.1
- [ ] v5.0
- Transport
- [x] TCP (unsecured)
- [x] TLS (secured, verifies the peer using a certificate authority file)
- Connect
- [x] Last Will
- [x] Message Retention
- [x] Authentication (username & password)
- [x] TLS encrypted connections
- [ ] Clean Session (can be set and sent, but the client has no persistence for QoS 2 messages)
- Publish
- [x] QoS Level 0
- [x] QoS Level 1 (limitation: no persisted state across sessions)
- [x] QoS Level 2 (limitation: no persisted state across sessions)
- Subscribe
- [x] QoS Level 0
- [x] QoS Level 1
- [x] QoS Level 2 (limitation: no persisted state across sessions)
- Supported Message Length: unlimited _(no limits enforced, although the MQTT protocol supports only up to 256MB which one shouldn't use even remotely anyway)_
- Logging possible (`Psr\Log\LoggerInterface` can be passed to the client)
- Persistence Drivers
- [x] In-Memory Driver
- [ ] Redis Driver
## Limitations
- Message flows with a QoS level higher than 0 are not persisted as the default implementation uses an in-memory repository for data.
To avoid issues with broken message flows, use the clean session flag to indicate that you don't care about old data.
It will not only instruct the broker to consider the connection new (without previous state), but will also reset the registered repository.
## Developing & Testing
### Certificates (TLS)
To run the tests (especially the TLS tests), you will need to create certificates. A command has been provided for this:
```sh
sh create-certificates.sh
```
This will create all required certificates in the `.ci/tls/` directory. The same script is used for continuous integration as well.
### MQTT Broker for Testing
Running the tests expects an MQTT broker to be running. The easiest way to run an MQTT broker is through Docker:
```sh
docker run --rm -it \
-p 1883:1883 \
-p 1884:1884 \
-p 8883:8883 \
-p 8884:8884 \
-v $(pwd)/.ci/tls:/mosquitto-certs \
-v $(pwd)/.ci/mosquitto.conf:/mosquitto/config/mosquitto.conf \
-v $(pwd)/.ci/mosquitto.passwd:/mosquitto/config/mosquitto.passwd \
eclipse-mosquitto:1.6
```
When run from the project directory, this will spawn a Mosquitto MQTT broker configured with the generated TLS certificates and a custom configuration.
In case you intend to run a different broker or using a different method, or use a public broker instead,
you will need to adjust the environment variables defined in `phpunit.xml` accordingly.
## License
`php-mqtt/client` is open-sourced software licensed under the [MIT license](LICENSE.md).

View File

@ -0,0 +1,53 @@
{
"name": "php-mqtt/client",
"description": "An MQTT client written in and for PHP.",
"type": "library",
"keywords": [
"mqtt",
"client",
"publish",
"subscribe"
],
"license": "MIT",
"authors": [
{
"name": "Marvin Mall",
"email": "marvin-mall@msn.com",
"role": "developer"
}
],
"autoload": {
"psr-4": {
"PhpMqtt\\Client\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"require": {
"php": "^8.0",
"psr/log": "^1.1|^2.0|^3.0",
"myclabs/php-enum": "^1.7"
},
"require-dev": {
"phpunit/php-invoker": "^3.0",
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.5"
},
"suggest": {
"ext-redis": "Required for the RedisRepository"
},
"scripts": {
"fix:cs": "vendor/bin/phpcbf",
"test": [
"@test:cs",
"@test:all"
],
"test:all": "vendor/bin/phpunit --testdox --log-junit=phpunit.report-junit.xml --coverage-clover=phpunit.coverage-clover.xml --coverage-text",
"test:cs": "vendor/bin/phpcs",
"test:feature": "vendor/bin/phpunit --testsuite=Feature --testdox --log-junit=phpunit.report-junit.xml --coverage-clover=phpunit.coverage-clover.xml --coverage-text",
"test:unit": "vendor/bin/phpunit --testsuite=Unit --testdox --log-junit=phpunit.report-junit.xml --coverage-clover=phpunit.coverage-clover.xml --coverage-text"
}
}

View File

@ -0,0 +1,30 @@
#!/bin/sh
# Generate a new CA certificate and key.
openssl genrsa -out .ci/tls/ca.key 2048
openssl req -x509 -new -nodes -key .ci/tls/ca.key -days 1 -out .ci/tls/ca.crt -subj "/C=AT/ST=Vorarlberg/CN=php-mqtt Test CA"
# Copy ca.crt to a file named by the hashed subject of the certificate. This is required for PHP's capath option to find the certificate.
cp .ci/tls/ca.crt .ci/tls/$(openssl x509 -hash -noout -in .ci/tls/ca.crt).0
# Create a Java Trust Store from the CA certificate. This is used by HiveMQ.
keytool -import -file .ci/tls/ca.crt -alias ca -keystore .ci/tls/ca.jks -storepass s3cr3t -trustcacerts -noprompt
# Generate a new server certificate and key, signed by the created CA.
openssl genrsa -out .ci/tls/server.key 2048
openssl req -new -key .ci/tls/server.key -out .ci/tls/server.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost"
openssl x509 -req -in .ci/tls/server.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/server.crt -days 1 -sha512
# Generate a Java Key Store from the server certificate. This is used by HiveMQ.
openssl pkcs12 -export -in .ci/tls/server.crt -inkey .ci/tls/server.key -out .ci/tls/server.p12 -passout pass:s3cr3t
keytool -importkeystore -srckeystore .ci/tls/server.p12 -srcstoretype PKCS12 -destkeystore .ci/tls/server.jks -deststoretype JKS -srcstorepass s3cr3t -deststorepass s3cr3t -noprompt
# Generate a client certificate without passphrase, signed by the created CA.
openssl genrsa -out .ci/tls/client.key 2048
openssl req -new -key .ci/tls/client.key -out .ci/tls/client.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost"
openssl x509 -req -in .ci/tls/client.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/client.crt -days 1 -sha256
# Generate a client certificate with passphrase, signed by the created CA.
openssl genrsa -aes128 -passout pass:s3cr3t -out .ci/tls/client2.key 2048
openssl req -new -key .ci/tls/client2.key -passin pass:s3cr3t -out .ci/tls/client2.csr -sha512 -subj "/C=AT/ST=Vorarlberg/CN=localhost"
openssl x509 -req -in .ci/tls/client2.csr -CA .ci/tls/ca.crt -CAkey .ci/tls/ca.key -CAcreateserial -out .ci/tls/client2.crt -days 1 -sha256

34
api/vendor/php-mqtt/client/phpunit.xml vendored Normal file
View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
enforceTimeLimit="true"
defaultTimeLimit="3"
timeoutForSmallTests="2"
timeoutForMediumTests="5"
timeoutForLargeTests="10"
>
<php>
<env name="MQTT_BROKER_HOST" value="localhost"/>
<env name="MQTT_BROKER_PORT" value="1883"/>
<env name="MQTT_BROKER_PORT_WITH_AUTHENTICATION" value="1884"/>
<env name="MQTT_BROKER_TLS_PORT" value="8883"/>
<env name="MQTT_BROKER_TLS_WITH_CLIENT_CERT_PORT" value="8884"/>
<env name="TLS_CERT_DIR" value=".ci/tls"/>
<env name="SKIP_TLS_TESTS" value="false"/>
</php>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">tests/Feature</directory>
</testsuite>
</testsuites>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>

View File

@ -0,0 +1,18 @@
sonar.organization=php-mqtt
sonar.projectKey=php-mqtt_client
# Paths are relative to the sonar-project.properties file.
sonar.sources=src
sonar.tests=tests
# Test report and code coverage related settings.
sonar.php.tests.reportPath=phpunit.report-junit.xml
sonar.php.coverage.reportPaths=phpunit.coverage-clover.xml
# Encoding of the source code. Default is default system encoding.
sonar.sourceEncoding=UTF-8
# Links for sonarcloud.io page.
sonar.links.ci=https://github.com/php-mqtt/client/actions
sonar.links.scm=https://github.com/php-mqtt/client
sonar.links.issue=https://github.com/php-mqtt/client/issues

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
/**
* Provides common methods used to generate random client ids.
*
* @package PhpMqtt\Client\Concerns
*/
trait GeneratesRandomClientIds
{
/**
* Generates a random client id in the form of an md5 hash.
*/
protected function generateRandomClientId(): string
{
return substr(md5(uniqid((string) random_int(0, PHP_INT_MAX), true)), 0, 20);
}
}

View File

@ -0,0 +1,301 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
use PhpMqtt\Client\Contracts\MqttClient;
/**
* Contains common methods and properties necessary to offer hooks.
*
* @mixin MqttClient
* @package PhpMqtt\Client\Concerns
*/
trait OffersHooks
{
/** @var \SplObjectStorage|array<\Closure> */
private $loopEventHandlers;
/** @var \SplObjectStorage|array<\Closure> */
private $publishEventHandlers;
/** @var \SplObjectStorage|array<\Closure> */
private $messageReceivedEventHandlers;
/** @var \SplObjectStorage|array<\Closure> */
private $connectedEventHandlers;
/**
* Needs to be called in order to initialize the trait.
*/
protected function initializeEventHandlers(): void
{
$this->loopEventHandlers = new \SplObjectStorage();
$this->publishEventHandlers = new \SplObjectStorage();
$this->messageReceivedEventHandlers = new \SplObjectStorage();
$this->connectedEventHandlers = new \SplObjectStorage();
}
/**
* Registers a loop event handler which is called each iteration of the loop.
* This event handler can be used for example to interrupt the loop under
* certain conditions.
*
* The loop event handler is passed the MQTT client instance as first and
* the elapsed time which the loop is already running for as second
* parameter. The elapsed time is a float containing seconds.
*
* Example:
* ```php
* $mqtt->registerLoopEventHandler(function (
* MqttClient $mqtt,
* float $elapsedTime
* ) use ($logger) {
* $logger->info("Running for [{$elapsedTime}] seconds already.");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerLoopEventHandler(\Closure $callback): MqttClient
{
$this->loopEventHandlers->attach($callback);
/** @var MqttClient $this */
return $this;
}
/**
* Unregisters a loop event handler which prevents it from being called
* in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterLoopEventHandler(?\Closure $callback = null): MqttClient
{
if ($callback === null) {
$this->loopEventHandlers->removeAll($this->loopEventHandlers);
} else {
$this->loopEventHandlers->detach($callback);
}
/** @var MqttClient $this */
return $this;
}
/**
* Runs all registered loop event handlers with the given parameters.
* Each event handler is executed in a try-catch block to avoid spilling exceptions.
*/
private function runLoopEventHandlers(float $elapsedTime): void
{
foreach ($this->loopEventHandlers as $handler) {
try {
call_user_func($handler, $this, $elapsedTime);
} catch (\Throwable $e) {
$this->logger->error('Loop hook callback threw exception.', ['exception' => $e]);
}
}
}
/**
* Registers a loop event handler which is called when a message is published.
*
* The loop event handler is passed the MQTT client as first, the topic as
* second and the message as third parameter. As fourth parameter, the message identifier
* will be passed, which can be null in case of QoS 0. The QoS level as well as the retained
* flag will also be passed as fifth and sixth parameters.
*
* Example:
* ```php
* $mqtt->registerPublishEventHandler(function (
* MqttClient $mqtt,
* string $topic,
* string $message,
* ?int $messageId,
* int $qualityOfService,
* bool $retain
* ) use ($logger) {
* $logger->info("Sending message on topic [{$topic}]: {$message}");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerPublishEventHandler(\Closure $callback): MqttClient
{
$this->publishEventHandlers->attach($callback);
/** @var MqttClient $this */
return $this;
}
/**
* Unregisters a publish event handler which prevents it from being called
* in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterPublishEventHandler(?\Closure $callback = null): MqttClient
{
if ($callback === null) {
$this->publishEventHandlers->removeAll($this->publishEventHandlers);
} else {
$this->publishEventHandlers->detach($callback);
}
/** @var MqttClient $this */
return $this;
}
/**
* Runs all the registered publish event handlers with the given parameters.
* Each event handler is executed in a try-catch block to avoid spilling exceptions.
*/
private function runPublishEventHandlers(string $topic, string $message, ?int $messageId, int $qualityOfService, bool $retain): void
{
foreach ($this->publishEventHandlers as $handler) {
try {
call_user_func($handler, $this, $topic, $message, $messageId, $qualityOfService, $retain);
} catch (\Throwable $e) {
$this->logger->error('Publish hook callback threw exception for published message on topic [{topic}].', [
'topic' => $topic,
'exception' => $e,
]);
}
}
}
/**
* Registers an event handler which is called when a message is received from the broker.
*
* The message received event handler is passed the MQTT client as first, the topic as
* second and the message as third parameter. As fourth parameter, the QoS level will be
* passed and the retained flag as fifth.
*
* Example:
* ```php
* $mqtt->registerReceivedMessageEventHandler(function (
* MqttClient $mqtt,
* string $topic,
* string $message,
* int $qualityOfService,
* bool $retained
* ) use ($logger) {
* $logger->info("Received message on topic [{$topic}]: {$message}");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerMessageReceivedEventHandler(\Closure $callback): MqttClient
{
$this->messageReceivedEventHandlers->attach($callback);
/** @var MqttClient $this */
return $this;
}
/**
* Unregisters a message received event handler which prevents it from being called in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterMessageReceivedEventHandler(?\Closure $callback = null): MqttClient
{
if ($callback === null) {
$this->messageReceivedEventHandlers->removeAll($this->messageReceivedEventHandlers);
} else {
$this->messageReceivedEventHandlers->detach($callback);
}
/** @var MqttClient $this */
return $this;
}
/**
* Runs all the registered message received event handlers with the given parameters.
* Each event handler is executed in a try-catch block to avoid spilling exceptions.
*/
private function runMessageReceivedEventHandlers(string $topic, string $message, int $qualityOfService, bool $retained): void
{
foreach ($this->messageReceivedEventHandlers as $handler) {
try {
call_user_func($handler, $this, $topic, $message, $qualityOfService, $retained);
} catch (\Throwable $e) {
$this->logger->error('Received message hook callback threw exception for received message on topic [{topic}].', [
'topic' => $topic,
'exception' => $e,
]);
}
}
}
/**
* Registers an event handler which is called when the client established a connection to the broker.
* This also includes manual reconnects as well as auto-reconnects by the client itself.
*
* The event handler is passed the MQTT client as first argument,
* followed by a flag which indicates whether an auto-reconnect occurred as second argument.
*
* Example:
* ```php
* $mqtt->registerConnectedEventHandler(function (
* MqttClient $mqtt,
* bool $isAutoReconnect
* ) use ($logger) {
* if ($isAutoReconnect) {
* $logger->info("Client successfully auto-reconnected to the broker.);
* } else {
* $logger->info("Client successfully connected to the broker.");
* }
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerConnectedEventHandler(\Closure $callback): MqttClient
{
$this->connectedEventHandlers->attach($callback);
/** @var MqttClient $this */
return $this;
}
/**
* Unregisters a connected event handler which prevents it from being called in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterConnectedEventHandler(?\Closure $callback = null): MqttClient
{
if ($callback === null) {
$this->connectedEventHandlers->removeAll($this->connectedEventHandlers);
} else {
$this->connectedEventHandlers->detach($callback);
}
/** @var MqttClient $this */
return $this;
}
/**
* Runs all the registered connected event handlers.
* Each event handler is executed in a try-catch block to avoid spilling exceptions.
*/
private function runConnectedEventHandlers(bool $isAutoReconnect): void
{
foreach ($this->connectedEventHandlers as $handler) {
try {
call_user_func($handler, $this, $isAutoReconnect);
} catch (\Throwable $e) {
$this->logger->error('Connected hook callback threw exception.', ['exception' => $e]);
}
}
}
}

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
/**
* Provides common methods to encode data before sending it to a broker
* and to decode data received from a broker.
*
* @package PhpMqtt\Client\Concerns
*/
trait TranscodesData
{
/**
* Creates a string which is prefixed with its own length as bytes.
* This means a string like 'hello world' will become
*
* \x00\x0bhello world
*
* where \x00\0x0b is the hex representation of 00000000 00001011 = 11
*/
protected function buildLengthPrefixedString(string $data): string
{
$length = strlen($data);
$msb = $length >> 8;
$lsb = $length % 256;
return chr($msb) . chr($lsb) . $data;
}
/**
* Converts the given string to a number, assuming it is an MSB encoded message id.
* MSB means preceding characters have higher value.
*/
protected function decodeMessageId(string $encodedMessageId): int
{
$length = strlen($encodedMessageId);
$result = 0;
foreach (str_split($encodedMessageId) as $index => $char) {
$result += ord($char) << (($length - 1) * 8 - ($index * 8));
}
return $result;
}
/**
* Encodes the given message identifier as string.
*/
protected function encodeMessageId(int $messageId): string
{
return chr($messageId >> 8) . chr($messageId % 256);
}
/**
* Encodes the length of a message as string, so it can be transmitted
* over the wire.
*/
protected function encodeMessageLength(int $length): string
{
$result = '';
do {
$digit = $length % 128;
$length = $length >> 7;
// if there are more digits to encode, set the top bit of this digit
if ($length > 0) {
$digit = ($digit | 0x80);
}
$result .= chr($digit);
} while ($length > 0);
return $result;
}
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\ConfigurationInvalidException;
use PhpMqtt\Client\MqttClient;
/**
* Provides methods to validate the configuration of an {@see MqttClient} and
* the {@see ConnectionSettings} being used to connect to a broker.
*
* @package PhpMqtt\Client\Concerns
*/
trait ValidatesConfiguration
{
/**
* Ensures the given connection settings are valid. If they are not valid,
* which means they are misconfigured, an exception containing information about
* the configuration error is thrown.
*
* @throws ConfigurationInvalidException
*/
protected function ensureConnectionSettingsAreValid(ConnectionSettings $settings): void
{
if ($settings->getConnectTimeout() < 1) {
throw new ConfigurationInvalidException('The connect timeout cannot be less than 1 second.');
}
if ($settings->getSocketTimeout() < 1) {
throw new ConfigurationInvalidException('The socket timeout cannot be less than 1 second.');
}
if ($settings->getResendTimeout() < 1) {
throw new ConfigurationInvalidException('The resend timeout cannot be less than 1 second.');
}
if ($settings->getKeepAliveInterval() < 1 || $settings->getKeepAliveInterval() > 65535) {
throw new ConfigurationInvalidException('The keep alive interval must be a value in the range of 1 to 65535 seconds.');
}
if ($settings->getMaxReconnectAttempts() < 1) {
throw new ConfigurationInvalidException('The maximum reconnect attempts cannot be fewer than 1.');
}
if ($settings->getDelayBetweenReconnectAttempts() < 0) {
throw new ConfigurationInvalidException('The delay between reconnect attempts cannot be lower than 0.');
}
if ($settings->getUsername() !== null && trim($settings->getUsername()) === '') {
throw new ConfigurationInvalidException('The username may not consist of white space only.');
}
if ($settings->getLastWillTopic() !== null && trim($settings->getLastWillTopic()) === '') {
throw new ConfigurationInvalidException('The last will topic may not consist of white space only.');
}
if ($settings->getLastWillQualityOfService() < MqttClient::QOS_AT_MOST_ONCE
|| $settings->getLastWillQualityOfService() > MqttClient::QOS_EXACTLY_ONCE) {
throw new ConfigurationInvalidException('The QoS for the last will must be a value in the range of 0 to 2.');
}
if ($settings->getTlsCertificateAuthorityFile() !== null && !is_file($settings->getTlsCertificateAuthorityFile())) {
throw new ConfigurationInvalidException('The Certificate Authority file setting must contain the path to a regular file.');
}
if ($settings->getTlsCertificateAuthorityPath() !== null && !is_dir($settings->getTlsCertificateAuthorityPath())) {
throw new ConfigurationInvalidException('The Certificate Authority path setting must contain the path to a directory.');
}
if ($settings->getTlsClientCertificateFile() !== null && !is_file($settings->getTlsClientCertificateFile())) {
throw new ConfigurationInvalidException('The client certificate file setting must contain the path to a regular file.');
}
if ($settings->getTlsClientCertificateKeyFile() !== null && !is_file($settings->getTlsClientCertificateKeyFile())) {
throw new ConfigurationInvalidException('The client certificate key file setting must contain the path to a regular file.');
}
if ($settings->getTlsClientCertificateKeyFile() !== null && $settings->getTlsClientCertificateFile() === null) {
throw new ConfigurationInvalidException('Using a client certificate key file without certificate does not work.');
}
if ($settings->getTlsClientCertificateKeyPassphrase() !== null && $settings->getTlsClientCertificateKeyFile() === null) {
throw new ConfigurationInvalidException('Using a client certificate key passphrase without key file does not work.');
}
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Concerns;
/**
* Provides common methods to work with buffers.
*
* @package PhpMqtt\Client\Concerns
*/
trait WorksWithBuffers
{
/**
* Pops the first $limit bytes from the given buffer and returns them.
*/
protected function pop(string &$buffer, int $limit): string
{
$limit = min(strlen($buffer), $limit);
$result = substr($buffer, 0, $limit);
$buffer = substr($buffer, $limit);
return $result;
}
}

View File

@ -0,0 +1,555 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
/**
* The settings used during connection to a broker.
*
* This class is immutable and all setters return a clone of the original class because
* connection settings must not change once passed to MqttClient.
*
* @package PhpMqtt\Client
*/
class ConnectionSettings
{
private ?string $username = null;
private ?string $password = null;
private bool $useBlockingSocket = false;
private int $connectTimeout = 60;
private int $socketTimeout = 5;
private int $resendTimeout = 10;
private int $keepAliveInterval = 10;
private bool $reconnectAutomatically = false;
private int $maxReconnectAttempts = 3;
private int $delayBetweenReconnectAttempts = 0;
private ?string $lastWillTopic = null;
private ?string $lastWillMessage = null;
private int $lastWillQualityOfService = 0;
private bool $lastWillRetain = false;
private bool $useTls = false;
private bool $tlsVerifyPeer = true;
private bool $tlsVerifyPeerName = true;
private bool $tlsSelfSignedAllowed = false;
private ?string $tlsCertificateAuthorityFile = null;
private ?string $tlsCertificateAuthorityPath = null;
private ?string $tlsClientCertificateFile = null;
private ?string $tlsClientCertificateKeyFile = null;
private ?string $tlsClientCertificateKeyPassphrase = null;
private ?string $tlsAlpn = null;
/**
* The username used for authentication when connecting to the broker.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setUsername(?string $username): ConnectionSettings
{
$copy = clone $this;
$copy->username = $username;
return $copy;
}
public function getUsername(): ?string
{
return $this->username;
}
/**
* The password used for authentication when connecting to the broker.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setPassword(?string $password): ConnectionSettings
{
$copy = clone $this;
$copy->password = $password;
return $copy;
}
public function getPassword(): ?string
{
return $this->password;
}
/**
* Whether to use a blocking socket when publishing messages or not.
* Normally, this setting can be ignored. When publishing large messages with multiple kilobytes in size,
* a blocking socket may be required if the receipt buffer of the broker is not large enough.
*
* Note: This setting has no effect on subscriptions, only on the publishing of messages.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function useBlockingSocket(bool $useBlockingSocket): ConnectionSettings
{
$copy = clone $this;
$copy->useBlockingSocket = $useBlockingSocket;
return $copy;
}
public function shouldUseBlockingSocket(): bool
{
return $this->useBlockingSocket;
}
/**
* The connect timeout is the maximum amount of seconds the client will try to establish
* a socket connection with the broker. The value cannot be less than 1 second.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setConnectTimeout(int $connectTimeout): ConnectionSettings
{
$copy = clone $this;
$copy->connectTimeout = $connectTimeout;
return $copy;
}
public function getConnectTimeout(): int
{
return $this->connectTimeout;
}
/**
* The socket timeout is the maximum amount of idle time in seconds for the socket connection.
* If no data is read or sent for the given amount of seconds, the socket will be closed.
* The value cannot be less than 1 second.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setSocketTimeout(int $socketTimeout): ConnectionSettings
{
$copy = clone $this;
$copy->socketTimeout = $socketTimeout;
return $copy;
}
public function getSocketTimeout(): int
{
return $this->socketTimeout;
}
/**
* The resend timeout is the number of seconds the client will wait before sending a duplicate
* of pending messages without acknowledgement. The value cannot be less than 1 second.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setResendTimeout(int $resendTimeout): ConnectionSettings
{
$copy = clone $this;
$copy->resendTimeout = $resendTimeout;
return $copy;
}
public function getResendTimeout(): int
{
return $this->resendTimeout;
}
/**
* The keep alive interval is the number of seconds the client will wait without sending a message
* until it sends a keep alive signal (ping) to the broker. The value cannot be less than 1 second
* and may not be higher than 65535 seconds. A reasonable value is 10 seconds (the default).
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setKeepAliveInterval(int $keepAliveInterval): ConnectionSettings
{
$copy = clone $this;
$copy->keepAliveInterval = $keepAliveInterval;
return $copy;
}
public function getKeepAliveInterval(): int
{
return $this->keepAliveInterval;
}
/**
* This flag determines whether the client will try to reconnect automatically,
* if it notices a disconnect while sending data.
* The setting cannot be used together with the clean session flag.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setReconnectAutomatically(bool $reconnectAutomatically): ConnectionSettings
{
$copy = clone $this;
$copy->reconnectAutomatically = $reconnectAutomatically;
return $copy;
}
public function shouldReconnectAutomatically(): bool
{
return $this->reconnectAutomatically;
}
/**
* Defines the maximum number of reconnect attempts until the client gives up. This setting
* is only relevant if {@see setReconnectAutomatically()} is set to true.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setMaxReconnectAttempts(int $maxReconnectAttempts): ConnectionSettings
{
$copy = clone $this;
$copy->maxReconnectAttempts = $maxReconnectAttempts;
return $copy;
}
public function getMaxReconnectAttempts(): int
{
return $this->maxReconnectAttempts;
}
/**
* Defines the delay between reconnect attempts in milliseconds.
* This setting is only relevant if {@see setReconnectAutomatically()} is set to true.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setDelayBetweenReconnectAttempts(int $delayBetweenReconnectAttempts): ConnectionSettings
{
$copy = clone $this;
$copy->delayBetweenReconnectAttempts = $delayBetweenReconnectAttempts;
return $copy;
}
public function getDelayBetweenReconnectAttempts(): int
{
return $this->delayBetweenReconnectAttempts;
}
/**
* If the broker should publish a last will message in the name of the client when the client
* disconnects abruptly, this setting defines the topic on which the message will be published.
*
* A last will message will only be published if both this setting as well as the last will
* message are configured.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setLastWillTopic(?string $lastWillTopic): ConnectionSettings
{
$copy = clone $this;
$copy->lastWillTopic = $lastWillTopic;
return $copy;
}
public function getLastWillTopic(): ?string
{
return $this->lastWillTopic;
}
/**
* If the broker should publish a last will message in the name of the client when the client
* disconnects abruptly, this setting defines the message which will be published.
*
* A last will message will only be published if both this setting as well as the last will
* topic are configured.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setLastWillMessage(?string $lastWillMessage): ConnectionSettings
{
$copy = clone $this;
$copy->lastWillMessage = $lastWillMessage;
return $copy;
}
public function getLastWillMessage(): ?string
{
return $this->lastWillMessage;
}
/**
* Determines whether the client has a last will.
*/
public function hasLastWill(): bool
{
return $this->lastWillTopic !== null && $this->lastWillMessage !== null;
}
/**
* The quality of service level the last will message of the client will be published with,
* if it gets triggered.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setLastWillQualityOfService(int $lastWillQualityOfService): ConnectionSettings
{
$copy = clone $this;
$copy->lastWillQualityOfService = $lastWillQualityOfService;
return $copy;
}
public function getLastWillQualityOfService(): int
{
return $this->lastWillQualityOfService;
}
/**
* This flag determines if the last will message of the client will be retained, if it gets
* triggered. Using this setting can be handy to signal that a client is offline by publishing
* a retained offline state in the last will and an online state as first message on connect.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setRetainLastWill(bool $lastWillRetain): ConnectionSettings
{
$copy = clone $this;
$copy->lastWillRetain = $lastWillRetain;
return $copy;
}
public function shouldRetainLastWill(): bool
{
return $this->lastWillRetain;
}
/**
* This flag determines if TLS should be used for the connection. The port which is used to
* connect to the broker must support TLS connections.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setUseTls(bool $useTls): ConnectionSettings
{
$copy = clone $this;
$copy->useTls = $useTls;
return $copy;
}
public function shouldUseTls(): bool
{
return $this->useTls;
}
/**
* This flag determines if the peer certificate is verified, if TLS is used.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsVerifyPeer(bool $tlsVerifyPeer): ConnectionSettings
{
$copy = clone $this;
$copy->tlsVerifyPeer = $tlsVerifyPeer;
return $copy;
}
public function shouldTlsVerifyPeer(): bool
{
return $this->tlsVerifyPeer;
}
/**
* This flag determines if the peer name is verified, if TLS is used.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsVerifyPeerName(bool $tlsVerifyPeerName): ConnectionSettings
{
$copy = clone $this;
$copy->tlsVerifyPeerName = $tlsVerifyPeerName;
return $copy;
}
public function shouldTlsVerifyPeerName(): bool
{
return $this->tlsVerifyPeerName;
}
/**
* This flag determines if self signed certificates of the peer should be accepted.
* Setting this to TRUE implies a security risk and should be avoided for production
* scenarios and public services.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsSelfSignedAllowed(bool $tlsSelfSignedAllowed): ConnectionSettings
{
$copy = clone $this;
$copy->tlsSelfSignedAllowed = $tlsSelfSignedAllowed;
return $copy;
}
public function isTlsSelfSignedAllowed(): bool
{
return $this->tlsSelfSignedAllowed;
}
/**
* The path to a Certificate Authority certificate which is used to verify the peer
* certificate, if TLS is used.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsCertificateAuthorityFile(?string $tlsCertificateAuthorityFile): ConnectionSettings
{
$copy = clone $this;
$copy->tlsCertificateAuthorityFile = $tlsCertificateAuthorityFile;
return $copy;
}
public function getTlsCertificateAuthorityFile(): ?string
{
return $this->tlsCertificateAuthorityFile;
}
/**
* The path to a directory containing Certificate Authority certificates which are
* used to verify the peer certificate, if TLS is used.
*
* Certificate files in this directory must be named by the hash of the certificate,
* ending with ".0" (without quotes). The certificate hash can be retrieved using the
* openssl_x509_parse() function, which returns an array. The hash can be found in the
* array under the key "hash".
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsCertificateAuthorityPath(?string $tlsCertificateAuthorityPath): ConnectionSettings
{
$copy = clone $this;
$copy->tlsCertificateAuthorityPath = $tlsCertificateAuthorityPath;
return $copy;
}
public function getTlsCertificateAuthorityPath(): ?string
{
return $this->tlsCertificateAuthorityPath;
}
/**
* The path to a client certificate file used for authentication, if TLS is used.
*
* The client certificate must be PEM encoded. It may optionally contain the
* certificate chain of issuers. The certificate key can be included in this certificate
* file or in a separate file ({@see ConnectionSettings::setTlsClientCertificateKeyFile()}).
* A passphrase can be configured using {@see ConnectionSettings::setTlsClientCertificateKeyPassphrase()}.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsClientCertificateFile(?string $tlsClientCertificateFile): ConnectionSettings
{
$copy = clone $this;
$copy->tlsClientCertificateFile = $tlsClientCertificateFile;
return $copy;
}
public function getTlsClientCertificateFile(): ?string
{
return $this->tlsClientCertificateFile;
}
/**
* The path to a client certificate key file used for authentication, if TLS is used.
*
* This option requires {@see ConnectionSettings::setTlsClientCertificateFile()}
* to be used as well.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsClientCertificateKeyFile(?string $tlsClientCertificateKeyFile): ConnectionSettings
{
$copy = clone $this;
$copy->tlsClientCertificateKeyFile = $tlsClientCertificateKeyFile;
return $copy;
}
public function getTlsClientCertificateKeyFile(): ?string
{
return $this->tlsClientCertificateKeyFile;
}
/**
* The passphrase used to decrypt the private key of the client certificate,
* which in return is used for authentication, if TLS is used.
*
* This option requires {@see ConnectionSettings::setTlsClientCertificateFile()}
* and {@see ConnectionSettings::setTlsClientCertificateKeyFile()} to be used as well.
*
* Please be aware that your passphrase is not stored in secure memory when using this option.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsClientCertificateKeyPassphrase(?string $tlsClientCertificateKeyPassphrase): ConnectionSettings
{
$copy = clone $this;
$copy->tlsClientCertificateKeyPassphrase = $tlsClientCertificateKeyPassphrase;
return $copy;
}
public function getTlsClientCertificateKeyPassphrase(): ?string
{
return $this->tlsClientCertificateKeyPassphrase;
}
/**
* The TLS ALPN is used to establish a TLS encrypted mqtt connection on port 443,
* which usually is reserved for TLS encrypted HTTP traffic.
*
* @return ConnectionSettings A copy of the original object with the new setting applied.
*/
public function setTlsAlpn(?string $tlsAlpn): ConnectionSettings
{
$copy = clone $this;
$copy->tlsAlpn = $tlsAlpn;
return $copy;
}
public function getTlsAlpn(): ?string
{
return $this->tlsAlpn;
}
}

View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Contracts;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\Exceptions\InvalidMessageException;
use PhpMqtt\Client\Exceptions\MqttClientException;
use PhpMqtt\Client\Exceptions\ProtocolViolationException;
use PhpMqtt\Client\Message;
use PhpMqtt\Client\Subscription;
/**
* Implementations of this interface provide message parsing capabilities.
* Services of this type are used by the {@see MqttClient} to implement multiple protocol versions.
*
* @package PhpMqtt\Client\Contracts
*/
interface MessageProcessor
{
/**
* Try to parse a message from the incoming buffer. If a message could be parsed successfully,
* the given message parameter is set to the parsed message and the result is true.
* If no message could be parsed, the result is false and the required bytes parameter indicates
* how many bytes are missing for the message to be complete. If this parameter is set to -1,
* it means we have no (or not yet) knowledge about the required bytes.
*/
public function tryFindMessageInBuffer(string $buffer, int $bufferLength, ?string &$message = null, int &$requiredBytes = -1): bool;
/**
* Parses and validates the given message based on its message type and contents.
* If no valid message could be found in the data, and no further action is required by the caller,
* null is returned.
*
* @throws InvalidMessageException
* @throws ProtocolViolationException
* @throws MqttClientException
*/
public function parseAndValidateMessage(string $message): ?Message;
/**
* Builds a connect message from the given connection settings, taking the protocol
* specifics into account.
*/
public function buildConnectMessage(ConnectionSettings $connectionSettings, bool $useCleanSession = false): string;
/**
* Builds a ping request message.
*/
public function buildPingRequestMessage(): string;
/**
* Builds a ping response message.
*/
public function buildPingResponseMessage(): string;
/**
* Builds a disconnect message.
*/
public function buildDisconnectMessage(): string;
/**
* Builds a subscribe message from the given parameters.
*
* @param Subscription[] $subscriptions
*/
public function buildSubscribeMessage(int $messageId, array $subscriptions, bool $isDuplicate = false): string;
/**
* Builds an unsubscribe message from the given parameters.
*
* @param string[] $topics
*/
public function buildUnsubscribeMessage(int $messageId, array $topics, bool $isDuplicate = false): string;
/**
* Builds a publish message based on the given parameters.
*/
public function buildPublishMessage(
string $topic,
string $message,
int $qualityOfService,
bool $retain,
?int $messageId = null,
bool $isDuplicate = false,
): string;
/**
* Builds a publish acknowledgement for the given message identifier.
*/
public function buildPublishAcknowledgementMessage(int $messageId): string;
/**
* Builds a publish received message for the given message identifier.
*/
public function buildPublishReceivedMessage(int $messageId): string;
/**
* Builds a publish release message for the given message identifier.
*/
public function buildPublishReleaseMessage(int $messageId): string;
/**
* Builds a publish complete message for the given message identifier.
*/
public function buildPublishCompleteMessage(int $messageId): string;
/**
* Handles the connect acknowledgement received from the broker. Exits normally if the
* connection could be established successfully according to the response. Throws an
* exception if the broker responded with an error.
*
* @throws ConnectingToBrokerFailedException
*/
public function handleConnectAcknowledgement(string $message): void;
}

View File

@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Contracts;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\ConfigurationInvalidException;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\Exceptions\DataTransferException;
use PhpMqtt\Client\Exceptions\InvalidMessageException;
use PhpMqtt\Client\Exceptions\MqttClientException;
use PhpMqtt\Client\Exceptions\ProtocolViolationException;
use PhpMqtt\Client\Exceptions\RepositoryException;
/**
* An interface for the MQTT client.
*
* @package PhpMqtt\Client\Contracts
*/
interface MqttClient
{
/**
* Connect to the MQTT broker using the given settings.
* If no custom settings are passed, the client will use the default settings.
* See {@see ConnectionSettings} for more details about the defaults.
*
* @throws ConfigurationInvalidException
* @throws ConnectingToBrokerFailedException
*/
public function connect(?ConnectionSettings $settings = null, bool $useCleanSession = false): void;
/**
* Sends a disconnect message to the broker and closes the socket.
*
* @throws DataTransferException
*/
public function disconnect(): void;
/**
* Returns an indication, whether the client is supposed to be connected already or not.
*
* Note: the result of this method should be used carefully, since we can only detect a
* closed socket once we try to send or receive data. Therefore, this method only gives
* an indication whether the client is in a connected state or not.
*
* This information may be useful in applications where multiple parts use the client.
*/
public function isConnected(): bool;
/**
* Publishes the given message on the given topic. If the additional quality of service
* and retention flags are set, the message will be published using these settings.
*
* @throws DataTransferException
* @throws RepositoryException
*/
public function publish(string $topic, string $message, int $qualityOfService = 0, bool $retain = false): void;
/**
* Subscribe to the given topic with the given quality of service.
*
* The subscription callback is passed the topic as first and the message as second
* parameter. A third parameter indicates whether the received message has been sent
* because it was retained by the broker. A fourth parameter contains matched topic wildcards.
*
* Example:
* ```php
* $mqtt->subscribe(
* '/foo/bar/+',
* function (string $topic, string $message, bool $retained, array $matchedWildcards) use ($logger) {
* $logger->info("Received {retained} message on topic [{topic}]: {message}", [
* 'topic' => $topic,
* 'message' => $message,
* 'retained' => $retained ? 'retained' : 'live'
* ]);
* }
* );
* ```
*
* If no callback is passed, a subscription will still be made. Received messages are delivered only to
* event handlers for received messages though.
*
* @throws DataTransferException
* @throws RepositoryException
*/
public function subscribe(string $topicFilter, ?callable $callback = null, int $qualityOfService = 0): void;
/**
* Unsubscribe from the given topic.
*
* @throws DataTransferException
* @throws RepositoryException
*/
public function unsubscribe(string $topicFilter): void;
/**
* Sets the interrupted signal. Doing so instructs the client to exit the loop, if it is
* actually looping.
*
* Sending multiple interrupt signals has no effect, unless the client exits the loop,
* which resets the signal for another loop.
*/
public function interrupt(): void;
/**
* Runs an event loop that handles messages from the server and calls the registered
* callbacks for published messages.
*
* If the second parameter is provided, the loop will exit as soon as all
* queues are empty. This means there may be no open subscriptions,
* no pending messages as well as acknowledgments and no pending unsubscribe requests.
*
* The third parameter will, if set, lead to a forceful exit after the specified
* amount of seconds, but only if the second parameter is set to true. This basically
* means that if we wait for all pending messages to be acknowledged, we only wait
* a maximum of $queueWaitLimit seconds until we give up. We do not exit after the
* given amount of time if there are open topic subscriptions though.
*
* @throws DataTransferException
* @throws InvalidMessageException
* @throws MqttClientException
* @throws ProtocolViolationException
*/
public function loop(bool $allowSleep = true, bool $exitWhenQueuesEmpty = false, ?int $queueWaitLimit = null): void;
/**
* Runs an event loop iteration that handles messages from the server and calls the registered
* callbacks for published messages. Also resends pending messages and calls loop event handlers.
*
* This method can be used to integrate the MQTT client in another event loop (like ReactPHP or Ratchet).
*
* Note: To ensure the event handlers called by this method will receive the correct elapsed time,
* the caller is responsible to provide the correct starting time of the loop as returned by `microtime(true)`.
*
* @throws DataTransferException
* @throws InvalidMessageException
* @throws MqttClientException
* @throws ProtocolViolationException
*/
public function loopOnce(float $loopStartedAt, bool $allowSleep = false, int $sleepMicroseconds = 100000): void;
/**
* Returns the host used by the client to connect to.
*/
public function getHost(): string;
/**
* Returns the port used by the client to connect to.
*/
public function getPort(): int;
/**
* Returns the identifier used by the client.
*/
public function getClientId(): string;
/**
* Returns the total number of received bytes, across reconnects.
*/
public function getReceivedBytes(): int;
/**
* Returns the total number of sent bytes, across reconnects.
*/
public function getSentBytes(): int;
/**
* Registers a loop event handler which is called each iteration of the loop.
* This event handler can be used for example to interrupt the loop under
* certain conditions.
*
* The loop event handler is passed the MQTT client instance as first and
* the elapsed time which the loop is already running for as second
* parameter. The elapsed time is a float containing seconds.
*
* Example:
* ```php
* $mqtt->registerLoopEventHandler(function (
* MqttClient $mqtt,
* float $elapsedTime
* ) use ($logger) {
* $logger->info("Running for [{$elapsedTime}] seconds already.");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerLoopEventHandler(\Closure $callback): MqttClient;
/**
* Unregisters a loop event handler which prevents it from being called
* in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterLoopEventHandler(?\Closure $callback = null): MqttClient;
/**
* Registers a loop event handler which is called when a message is published.
*
* The loop event handler is passed the MQTT client as first, the topic as
* second and the message as third parameter. As fourth parameter, the
* message identifier will be passed. The QoS level as well as the retained
* flag will also be passed as fifth and sixth parameters.
*
* Example:
* ```php
* $mqtt->registerPublishEventHandler(function (
* MqttClient $mqtt,
* string $topic,
* string $message,
* int $messageId,
* int $qualityOfService,
* bool $retain
* ) use ($logger) {
* $logger->info("Received message on topic [{$topic}]: {$message}");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerPublishEventHandler(\Closure $callback): MqttClient;
/**
* Unregisters a publish event handler which prevents it from being called
* in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterPublishEventHandler(?\Closure $callback = null): MqttClient;
/**
* Registers an event handler which is called when a message is received from the broker.
*
* The message received event handler is passed the MQTT client as first, the topic as
* second and the message as third parameter. As fourth parameter, the QoS level will be
* passed and the retained flag as fifth.
*
* Example:
* ```php
* $mqtt->registerReceivedMessageEventHandler(function (
* MqttClient $mqtt,
* string $topic,
* string $message,
* int $qualityOfService,
* bool $retained
* ) use ($logger) {
* $logger->info("Received message on topic [{$topic}]: {$message}");
* });
* ```
*
* Multiple event handlers can be registered at the same time.
*/
public function registerMessageReceivedEventHandler(\Closure $callback): MqttClient;
/**
* Unregisters a message received event handler which prevents it from being called in the future.
*
* This does not affect other registered event handlers. It is possible
* to unregister all registered event handlers by passing null as callback.
*/
public function unregisterMessageReceivedEventHandler(?\Closure $callback = null): MqttClient;
}

View File

@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Contracts;
use DateTime;
use PhpMqtt\Client\Exceptions\PendingMessageAlreadyExistsException;
use PhpMqtt\Client\Exceptions\PendingMessageNotFoundException;
use PhpMqtt\Client\Exceptions\RepositoryException;
use PhpMqtt\Client\PendingMessage;
use PhpMqtt\Client\Subscription;
/**
* Implementations of this interface provide storage capabilities to an MQTT client.
*
* Services of this type have three primary goals:
* 1. Providing and keeping track of message identifiers, since they must be unique
* within the message flow (i.e. there may not be duplicates of different messages
* at the same time).
* 2. Storing and keeping track of subscriptions, which is especially necessary in case
* of persisted sessions.
* 3. Storing and keeping track of pending messages (i.e. sent messages, which have not
* been acknowledged yet by the broker).
*
* @package PhpMqtt\Client\Contracts
*/
interface Repository
{
/**
* Re-initializes the repository by deleting all persisted data and restoring the original state,
* which was given when the repository was first created. This is used when a clean session
* is requested by a client during connection.
*/
public function reset(): void;
/**
* Returns a new message id. The message id might have been used before,
* but it is currently not being used (i.e. in a resend queue).
*
* @throws RepositoryException
*/
public function newMessageId(): int;
/**
* Returns the number of pending outgoing messages.
*/
public function countPendingOutgoingMessages(): int;
/**
* Gets a pending outgoing message with the given message identifier, if found.
*/
public function getPendingOutgoingMessage(int $messageId): ?PendingMessage;
/**
* Gets a list of pending outgoing messages last sent before the given date time.
*
* If date time is `null`, all pending messages are returned.
*
* The messages are returned in the same order they were added to the repository.
*
* @return PendingMessage[]
*/
public function getPendingOutgoingMessagesLastSentBefore(?DateTime $dateTime = null): array;
/**
* Adds a pending outgoing message to the repository.
*
* @throws PendingMessageAlreadyExistsException
*/
public function addPendingOutgoingMessage(PendingMessage $message): void;
/**
* Marks an existing pending outgoing published message as received in the repository.
*
* If the message does not exists, an exception is thrown,
* otherwise `true` is returned if the message was marked as received, and `false`
* in case it was already marked as received.
*
* @throws PendingMessageNotFoundException
*/
public function markPendingOutgoingPublishedMessageAsReceived(int $messageId): bool;
/**
* Removes a pending outgoing message from the repository.
*
* If a pending message with the given identifier is found and
* successfully removed from the repository, `true` is returned.
* Otherwise `false` will be returned.
*/
public function removePendingOutgoingMessage(int $messageId): bool;
/**
* Returns the number of pending incoming messages.
*/
public function countPendingIncomingMessages(): int;
/**
* Gets a pending incoming message with the given message identifier, if found.
*/
public function getPendingIncomingMessage(int $messageId): ?PendingMessage;
/**
* Adds a pending outgoing message to the repository.
*
* @throws PendingMessageAlreadyExistsException
*/
public function addPendingIncomingMessage(PendingMessage $message): void;
/**
* Removes a pending incoming message from the repository.
*
* If a pending message with the given identifier is found and
* successfully removed from the repository, `true` is returned.
* Otherwise `false` will be returned.
*/
public function removePendingIncomingMessage(int $messageId): bool;
/**
* Returns the number of registered subscriptions.
*/
public function countSubscriptions(): int;
/**
* Adds a subscription to the repository.
*/
public function addSubscription(Subscription $subscription): void;
/**
* Gets all subscriptions matching the given topic.
*
* @return Subscription[]
*/
public function getSubscriptionsMatchingTopic(string $topicName): array;
/**
* Removes the subscription with the given topic filter from the repository.
*
* Returns `true` if a topic subscription existed and has been removed.
* Otherwise, `false` is returned.
*/
public function removeSubscription(string $topicFilter): bool;
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client is not connected to a broker and tries
* to perform an action which requires a connection (e.g. publish or subscribe).
*
* @package PhpMqtt\Client\Exceptions
*/
class ClientNotConnectedToBrokerException extends DataTransferException
{
public const EXCEPTION_CONNECTION_LOST = 0300;
/**
* ClientNotConnectedToBrokerException constructor.
*/
public function __construct(string $error)
{
parent::__construct(self::EXCEPTION_CONNECTION_LOST, $error);
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client has been misconfigured or wrong connection
* settings are being used.
*
* @package PhpMqtt\Client\Exceptions
*/
class ConfigurationInvalidException extends MqttClientException
{
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client could not connect to the broker.
*
* @package PhpMqtt\Client\Exceptions
*/
class ConnectingToBrokerFailedException extends MqttClientException
{
public const EXCEPTION_CONNECTION_FAILED = 0001;
public const EXCEPTION_CONNECTION_PROTOCOL_VERSION = 0002;
public const EXCEPTION_CONNECTION_IDENTIFIER_REJECTED = 0003;
public const EXCEPTION_CONNECTION_BROKER_UNAVAILABLE = 0004;
public const EXCEPTION_CONNECTION_INVALID_CREDENTIALS = 0005;
public const EXCEPTION_CONNECTION_UNAUTHORIZED = 0006;
public const EXCEPTION_CONNECTION_SOCKET_ERROR = 1000;
public const EXCEPTION_CONNECTION_TLS_ERROR = 2000;
/**
* ConnectingToBrokerFailedException constructor.
*/
public function __construct(
int $code,
string $error,
private ?string $connectionErrorCode = null,
private ?string $connectionErrorMessage = null,
)
{
parent::__construct(
sprintf('[%s] Establishing a connection to the MQTT broker failed: %s', $code, $error),
$code
);
}
/**
* Retrieves the connection error code.
*/
public function getConnectionErrorCode(): ?string
{
return $this->connectionErrorCode;
}
/**
* Retrieves the connection error message.
*/
public function getConnectionErrorMessage(): ?string
{
return $this->connectionErrorMessage;
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client encountered an error while transferring data.
*
* @package PhpMqtt\Client\Exceptions
*/
class DataTransferException extends MqttClientException
{
public const EXCEPTION_TX_DATA = 0101;
public const EXCEPTION_RX_DATA = 0102;
/**
* DataTransferException constructor.
*/
public function __construct(int $code, string $error)
{
parent::__construct(
sprintf('[%s] Transferring data over socket failed: %s', $code, $error),
$code
);
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client encounters an invalid message.
*
* @package PhpMqtt\Client\Exceptions
*/
class InvalidMessageException extends MqttClientException
{
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client error occurs.
*
* @package PhpMqtt\Client\Exceptions
*/
class MqttClientException extends \Exception
{
/**
* MqttClientException constructor.
*/
public function __construct(string $message = '', int $code = 0, ?\Throwable $parentException = null)
{
if (empty($message)) {
parent::__construct(
sprintf('[%s] The MQTT client encountered an error.', $code),
$code,
$parentException
);
} else {
parent::__construct($message, $code, $parentException);
}
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if a pending message with the same packet identifier is still pending.
*
* @package PhpMqtt\Client\Exceptions
*/
class PendingMessageAlreadyExistsException extends RepositoryException
{
/**
* PendingMessageAlreadyExistsException constructor.
*/
public function __construct(int $messageId)
{
parent::__construct(sprintf('A pending message with the message identifier [%s] exists already.', $messageId));
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if a pending message with the same packet identifier is not found.
*
* @package PhpMqtt\Client\Exceptions
*/
class PendingMessageNotFoundException extends RepositoryException
{
/**
* PendingMessageNotFoundException constructor.
*/
public function __construct(int $messageId)
{
parent::__construct(sprintf('No pending message with the message identifier [%s].', $messageId));
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an invalid MQTT version is given.
*
* @package PhpMqtt\Client\Exceptions
*/
class ProtocolNotSupportedException extends MqttClientException
{
/**
* ProtocolNotSupportedException constructor.
*/
public function __construct(string $protocol)
{
parent::__construct(sprintf('The given protocol version [%s] is not supported.', $protocol));
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client encountered a protocol violation.
*
* @package PhpMqtt\Client\Exceptions
*/
class ProtocolViolationException extends MqttClientException
{
/**
* ProtocolViolationException constructor.
*/
public function __construct(string $error)
{
parent::__construct(sprintf('Protocol violation: %s.', $error));
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\Exceptions;
/**
* Exception to be thrown if an MQTT client repository encounters an error.
*
* @package PhpMqtt\Client\Exceptions
*/
class RepositoryException extends MqttClientException
{
}

View File

@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
/**
* Wrapper for another logger. Drops logged messages if no logger is available.
*
* @internal This class is not part of the public API of the library and used internally only.
* @package PhpMqtt\Client
*/
class Logger implements LoggerInterface
{
/**
* Logger constructor.
*
* @param LoggerInterface|null $logger
*/
public function __construct(
private string $host,
private int $port,
private string $clientId,
private ?LoggerInterface $logger = null,
)
{
}
/**
* System is unusable.
*
* @param string $message
* @param array $context
*/
public function emergency($message, array $context = []): void
{
$this->log(LogLevel::EMERGENCY, $message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
*/
public function alert($message, array $context = []): void
{
$this->log(LogLevel::ALERT, $message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
*/
public function critical($message, array $context = []): void
{
$this->log(LogLevel::CRITICAL, $message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
*/
public function error($message, array $context = []): void
{
$this->log(LogLevel::ERROR, $message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
*/
public function warning($message, array $context = []): void
{
$this->log(LogLevel::WARNING, $message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*/
public function notice($message, array $context = []): void
{
$this->log(LogLevel::NOTICE, $message, $context);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
*/
public function info($message, array $context = []): void
{
$this->log(LogLevel::INFO, $message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*/
public function debug($message, array $context = []): void
{
$this->log(LogLevel::DEBUG, $message, $context);
}
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*/
public function log($level, $message, array $context = []): void
{
if ($this->logger === null) {
return;
}
$this->logger->log($level, $this->wrapLogMessage($message), $this->mergeContext($context));
}
/**
* Wraps the given log message by prepending the client id and broker.
*/
protected function wrapLogMessage(string $message): string
{
return 'MQTT [{host}:{port}] [{clientId}] ' . $message;
}
/**
* Adds global context like host, port and client id to the log context.
*/
protected function mergeContext(array $context): array
{
return array_merge([
'host' => $this->host,
'port' => $this->port,
'clientId' => $this->clientId,
], $context);
}
}

View File

@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
use PhpMqtt\Client\Contracts\MessageProcessor;
use PhpMqtt\Client\Contracts\MqttClient;
/**
* Describes an action which is supposed to be performed after receiving a message.
* Objects of this type are used by the {@see MessageProcessor} to instruct the
* {@see MqttClient} about required steps to take.
*
* @package PhpMqtt\Client
*/
class Message
{
private ?int $messageId = null;
private ?string $topic = null;
private ?string $content = null;
/** @var int[] */
private array $acknowledgedQualityOfServices = [];
/**
* Message constructor.
*/
public function __construct(
private MessageType $type,
private int $qualityOfService = 0,
private bool $retained = false,
)
{
}
public function getType(): MessageType
{
return $this->type;
}
public function getQualityOfService(): int
{
return $this->qualityOfService;
}
public function getRetained(): bool
{
return $this->retained;
}
public function getMessageId(): ?int
{
return $this->messageId;
}
public function setMessageId(?int $messageId): Message
{
$this->messageId = $messageId;
return $this;
}
public function getTopic(): ?string
{
return $this->topic;
}
public function setTopic(?string $topic): Message
{
$this->topic = $topic;
return $this;
}
public function getContent(): ?string
{
return $this->content;
}
public function setContent(?string $content): Message
{
$this->content = $content;
return $this;
}
/**
* @return int[]
*/
public function getAcknowledgedQualityOfServices(): array
{
return $this->acknowledgedQualityOfServices;
}
/**
* @param int[] $acknowledgedQualityOfServices
*/
public function setAcknowledgedQualityOfServices(array $acknowledgedQualityOfServices): Message
{
$this->acknowledgedQualityOfServices = $acknowledgedQualityOfServices;
return $this;
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\MessageProcessors;
use PhpMqtt\Client\Concerns\TranscodesData;
use PhpMqtt\Client\Concerns\WorksWithBuffers;
use Psr\Log\LoggerInterface;
/**
* This message processor serves as base for other message processors, providing
* default implementations for some methods.
*
* @package PhpMqtt\Client\MessageProcessors
*/
abstract class BaseMessageProcessor
{
use TranscodesData;
use WorksWithBuffers;
public const QOS_AT_MOST_ONCE = 0;
public const QOS_AT_LEAST_ONCE = 1;
public const QOS_EXACTLY_ONCE = 2;
/**
* BaseMessageProcessor constructor.
*/
public function __construct(protected LoggerInterface $logger)
{
}
}

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\MessageProcessors;
use PhpMqtt\Client\Exceptions\InvalidMessageException;
use PhpMqtt\Client\Exceptions\ProtocolViolationException;
use PhpMqtt\Client\Message;
use PhpMqtt\Client\MessageType;
/**
* This message processor implements the MQTT protocol version 3.1.1.
*
* @package PhpMqtt\Client\MessageProcessors
*/
class Mqtt311MessageProcessor extends Mqtt31MessageProcessor
{
/**
* {@inheritDoc}
*/
protected function getEncodedProtocolNameAndVersion(): string
{
return $this->buildLengthPrefixedString('MQTT') . chr(0x04); // protocol version (4)
}
/**
* {@inheritDoc}
*/
public function parseAndValidateMessage(string $message): ?Message
{
$result = parent::parseAndValidateMessage($message);
if ($this->isPublishMessageWithNullCharacter($result)) {
throw new ProtocolViolationException('The broker sent us a message with the forbidden unicode character U+0000.');
}
return $result;
}
/**
* {@inheritDoc}
*/
protected function parseAndValidateSubscribeAcknowledgementMessage(string $data): Message
{
if (strlen($data) < 3) {
$this->logger->notice('Received invalid subscribe acknowledgement from the broker.');
throw new InvalidMessageException('Received invalid subscribe acknowledgement from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
// Parse and validate the QoS acknowledgements.
$acknowledgements = array_map('ord', str_split($data));
foreach ($acknowledgements as $acknowledgement) {
if (!in_array($acknowledgement, [0, 1, 2, 128])) {
throw new InvalidMessageException('Received subscribe acknowledgement with invalid QoS values from the broker.');
}
}
return (new Message(MessageType::SUBSCRIBE_ACKNOWLEDGEMENT()))
->setMessageId($messageId)
->setAcknowledgedQualityOfServices($acknowledgements);
}
/**
* Determines if the given message is a PUBLISH message and contains the unicode null character U+0000.
*/
private function isPublishMessageWithNullCharacter(?Message $message): bool
{
return $message !== null
&& $message->getType()->equals(MessageType::PUBLISH())
&& $message->getContent() !== null
&& preg_match('/\x{0000}/u', $message->getContent());
}
}

View File

@ -0,0 +1,712 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client\MessageProcessors;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Contracts\MessageProcessor;
use PhpMqtt\Client\Exceptions\ConnectingToBrokerFailedException;
use PhpMqtt\Client\Exceptions\InvalidMessageException;
use PhpMqtt\Client\Exceptions\ProtocolViolationException;
use PhpMqtt\Client\Message;
use PhpMqtt\Client\MessageType;
use Psr\Log\LoggerInterface;
/**
* This message processor implements the MQTT protocol version 3.1.
*
* @package PhpMqtt\Client\MessageProcessors
*/
class Mqtt31MessageProcessor extends BaseMessageProcessor implements MessageProcessor
{
/**
* Creates a new message processor instance which supports version 3.1 of the MQTT protocol.
*/
public function __construct(private string $clientId, LoggerInterface $logger)
{
parent::__construct($logger);
}
/**
* {@inheritDoc}
*/
public function tryFindMessageInBuffer(string $buffer, int $bufferLength, ?string &$message = null, int &$requiredBytes = -1): bool
{
// If we received no input, we can return immediately without doing work.
if ($bufferLength === 0) {
return false;
}
// If we received not at least the fixed header with one length indicating byte,
// we know that there can't be a valid message in the buffer. So we return early.
if ($bufferLength < 2) {
return false;
}
// Read the second byte of the message to get the remaining length.
// If the continuation bit (8) is set on the length byte, another byte will be read as length.
$byteIndex = 1;
$remainingLength = 0;
$multiplier = 1;
do {
// If the buffer has no more data, but we need to read more for the length header,
// we cannot give useful information about the remaining length and exit early.
if ($byteIndex + 1 > $bufferLength) {
return false;
}
// There can me a maximum of four bytes for the package length, which means we cann opt-out
// when reaching the 6th byte in the buffer. This is only a safety measure in case the broker
// is sending invalid messages. Normally, the loop exits on its own.
if ($byteIndex >= 6) {
break;
}
// Otherwise, we can take seven bits to calculate the length and the remaining eighth bit
// as continuation bit.
$digit = ord($buffer[$byteIndex]);
$remainingLength += ($digit & 127) * $multiplier;
$multiplier *= 128;
$byteIndex++;
} while (($digit & 128) !== 0);
// At this point, we can now tell whether the remaining length amount of bytes are available
// or not. If not, we return the amount of bytes required for the message to be complete.
$requiredBufferLength = $byteIndex + $remainingLength;
if ($requiredBufferLength > $bufferLength) {
$requiredBytes = $requiredBufferLength;
return false;
}
// Now that we have a full message in the buffer, we can set the output and return.
$message = substr($buffer, 0, $requiredBufferLength);
return true;
}
/**
* {@inheritDoc}
*/
public function buildConnectMessage(ConnectionSettings $connectionSettings, bool $useCleanSession = false): string
{
// The protocol name and version.
$buffer = $this->getEncodedProtocolNameAndVersion();
// Build connection flags based on the connection settings.
$buffer .= chr($this->buildConnectionFlags($connectionSettings, $useCleanSession));
// Encode and add the keep alive interval.
$buffer .= chr($connectionSettings->getKeepAliveInterval() >> 8);
$buffer .= chr($connectionSettings->getKeepAliveInterval() & 0xff);
// Encode and add the client identifier.
$buffer .= $this->buildLengthPrefixedString($this->clientId);
// Encode and add the last will topic and message, if configured.
if ($connectionSettings->hasLastWill()) {
$buffer .= $this->buildLengthPrefixedString($connectionSettings->getLastWillTopic());
$buffer .= $this->buildLengthPrefixedString($connectionSettings->getLastWillMessage());
}
// Encode and add the credentials, if configured.
if ($connectionSettings->getUsername() !== null) {
$buffer .= $this->buildLengthPrefixedString($connectionSettings->getUsername());
}
if ($connectionSettings->getPassword() !== null) {
$buffer .= $this->buildLengthPrefixedString($connectionSettings->getPassword());
}
// The header consists of the message type 0x10 and the length.
$header = chr(0x10) . $this->encodeMessageLength(strlen($buffer));
return $header . $buffer;
}
/**
* Returns the encoded protocol name and version, ready to be sent as part of the CONNECT message.
*/
protected function getEncodedProtocolNameAndVersion(): string
{
return $this->buildLengthPrefixedString('MQIsdp') . chr(0x03); // protocol version (3)
}
/**
* Builds the connection flags from the inputs and settings.
*
* The bit structure of the connection flags is as follows:
* 0 - reserved
* 1 - clean session flag
* 2 - last will flag
* 3 - QoS flag (1)
* 4 - QoS flag (2)
* 5 - retain last will flag
* 6 - password flag
* 7 - username flag
*
* @link http://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html#connect MQTT 3.1 Spec
*/
protected function buildConnectionFlags(ConnectionSettings $connectionSettings, bool $useCleanSession = false): int
{
$flags = 0;
if ($useCleanSession) {
$this->logger->debug('Using the [clean session] flag for the connection.');
$flags += 1 << 1;
}
if ($connectionSettings->hasLastWill()) {
$this->logger->debug('Using the [will] flag for the connection.');
$flags += 1 << 2;
if ($connectionSettings->getLastWillQualityOfService() > self::QOS_AT_MOST_ONCE) {
$this->logger->debug('Using last will QoS level [{qos}] for the connection.', [
'qos' => $connectionSettings->getLastWillQualityOfService(),
]);
$flags += $connectionSettings->getLastWillQualityOfService() << 3;
}
if ($connectionSettings->shouldRetainLastWill()) {
$this->logger->debug('Using the [retain last will] flag for the connection.');
$flags += 1 << 5;
}
}
if ($connectionSettings->getPassword() !== null) {
$this->logger->debug('Using the [password] flag for the connection.');
$flags += 1 << 6;
}
if ($connectionSettings->getUsername() !== null) {
$this->logger->debug('Using the [username] flag for the connection.');
$flags += 1 << 7;
}
return $flags;
}
/**
* {@inheritDoc}
*/
public function handleConnectAcknowledgement(string $message): void
{
if (strlen($message) !== 4 || ($messageType = ord($message[0]) >> 4) !== 2) {
$this->logger->error('Expected connect acknowledgement; received a different response.', ['messageType' => $messageType ?? null]);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_FAILED,
'A connection could not be established. Expected connect acknowledgement; received a different response else.'
);
}
$errorCode = ord($message[3]);
$logContext = ['errorCode' => sprintf('0x%02X', $errorCode)];
switch ($errorCode) {
case 0x00:
$this->logger->info('Connection with broker established successfully.', $logContext);
break;
case 0x01:
$this->logger->error('The broker does not support MQTT v3.1.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_PROTOCOL_VERSION,
'The configured broker does not support MQTT v3.1.'
);
case 0x02:
$this->logger->error('The broker rejected the sent identifier.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_IDENTIFIER_REJECTED,
'The configured broker rejected the sent identifier.'
);
case 0x03:
$this->logger->error('The broker is currently unavailable.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_BROKER_UNAVAILABLE,
'The configured broker is currently unavailable.'
);
case 0x04:
$this->logger->error('The broker reported the credentials as invalid.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_INVALID_CREDENTIALS,
'The configured broker reported the credentials as invalid.'
);
case 0x05:
$this->logger->error('The broker responded with unauthorized.', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_UNAUTHORIZED,
'The configured broker responded with unauthorized.'
);
default:
$this->logger->error('The broker responded with an invalid error code [{errorCode}].', $logContext);
throw new ConnectingToBrokerFailedException(
ConnectingToBrokerFailedException::EXCEPTION_CONNECTION_FAILED,
'The configured broker responded with an invalid error code. A connection could not be established.'
);
}
}
/**
* Builds a ping request message.
*/
public function buildPingRequestMessage(): string
{
// The message consists of the command 0xc0 and the length 0.
return chr(0xc0) . chr(0x00);
}
/**
* Builds a ping response message.
*/
public function buildPingResponseMessage(): string
{
// The message consists of the command 0xd0 and the length 0.
return chr(0xd0) . chr(0x00);
}
/**
* Builds a disconnect message.
*/
public function buildDisconnectMessage(): string
{
// The message consists of the command 0xe0 and the length 0.
return chr(0xe0) . chr(0x00);
}
/**
* {@inheritDoc}
*/
public function buildSubscribeMessage(int $messageId, array $subscriptions, bool $isDuplicate = false): string
{
// Encode the message id, it always consists of two bytes.
$buffer = $this->encodeMessageId($messageId);
foreach ($subscriptions as $subscription) {
// Encode the topic as length prefixed string.
$buffer .= $this->buildLengthPrefixedString($subscription->getTopicFilter());
// Encode the quality of service level.
$buffer .= chr($subscription->getQualityOfServiceLevel());
}
// The header consists of the message type 0x82 and the length.
$header = chr(0x82) . $this->encodeMessageLength(strlen($buffer));
return $header . $buffer;
}
/**
* {@inheritDoc}
*/
public function buildUnsubscribeMessage(int $messageId, array $topics, bool $isDuplicate = false): string
{
// Encode the message id, it always consists of two bytes.
$buffer = $this->encodeMessageId($messageId);
foreach ($topics as $topic) {
// Encode the topic as length prefixed string.
$buffer .= $this->buildLengthPrefixedString($topic);
}
// The header consists of the message type 0xa2 and the length.
// Additionally, the first byte may contain the duplicate flag.
$command = 0xa2 | ($isDuplicate ? 1 << 3 : 0);
$header = chr($command) . $this->encodeMessageLength(strlen($buffer));
return $header . $buffer;
}
/**
* {@inheritDoc}
*/
public function buildPublishMessage(
string $topic,
string $message,
int $qualityOfService,
bool $retain,
?int $messageId = null,
bool $isDuplicate = false,
): string
{
// Encode the topic as length prefixed string.
$buffer = $this->buildLengthPrefixedString($topic);
// Encode the message id, if given. It always consists of two bytes.
if ($messageId !== null)
{
$buffer .= $this->encodeMessageId($messageId);
}
// Add the message without encoding.
$buffer .= $message;
// Encode the command with supported flags.
$command = 0x30;
if ($retain) {
$command += 1 << 0;
}
if ($qualityOfService > self::QOS_AT_MOST_ONCE) {
$command += $qualityOfService << 1;
}
if ($qualityOfService > self::QOS_AT_MOST_ONCE && $isDuplicate) {
$command += 1 << 3;
}
// Build the header from the command and the encoded message length.
$header = chr($command) . $this->encodeMessageLength(strlen($buffer));
return $header . $buffer;
}
/**
* {@inheritDoc}
*/
public function buildPublishAcknowledgementMessage(int $messageId): string
{
return chr(0x40) . chr(0x02) . $this->encodeMessageId($messageId);
}
/**
* {@inheritDoc}
*/
public function buildPublishReceivedMessage(int $messageId): string
{
return chr(0x50) . chr(0x02) . $this->encodeMessageId($messageId);
}
/**
* {@inheritDoc}
*/
public function buildPublishReleaseMessage(int $messageId): string
{
return chr(0x62) . chr(0x02) . $this->encodeMessageId($messageId);
}
/**
* {@inheritDoc}
*/
public function buildPublishCompleteMessage(int $messageId): string
{
return chr(0x70) . chr(0x02) . $this->encodeMessageId($messageId);
}
/**
* {@inheritDoc}
*/
public function parseAndValidateMessage(string $message): ?Message
{
$qualityOfService = 0;
$retained = false;
$data = '';
$result = $this->tryDecodeMessage($message, $command, $qualityOfService, $retained, $data);
if ($result === false) {
throw new InvalidMessageException('The passed message could not be decoded.');
}
// Ensure the command is supported by this version of the protocol.
if ($command <= 0 || $command >= 15) {
$this->logger->error('Reserved command received from the broker. Supported are commands (including) 1-14.', [
'command' => $command,
]);
throw new InvalidMessageException('A reserved command has been used in the message.');
}
// Then handle the command accordingly.
switch ($command) {
case 0x02:
throw new ProtocolViolationException('Unexpected connection acknowledgement.');
case 0x03:
return $this->parseAndValidatePublishMessage($data, $qualityOfService, $retained);
case 0x04:
return $this->parseAndValidatePublishAcknowledgementMessage($data);
case 0x05:
return $this->parseAndValidatePublishReceiptMessage($data);
case 0x06:
return $this->parseAndValidatePublishReleaseMessage($data);
case 0x07:
return $this->parseAndValidatePublishCompleteMessage($data);
case 0x09:
return $this->parseAndValidateSubscribeAcknowledgementMessage($data);
case 0x0b:
return $this->parseAndValidateUnsubscribeAcknowledgementMessage($data);
case 0x0c:
return $this->parseAndValidatePingRequestMessage();
case 0x0d:
return $this->parseAndValidatePingAcknowledgementMessage();
default:
$this->logger->debug('Received message with unsupported command [{command}]. Skipping.', ['command' => $command]);
break;
}
// If we arrive here, we must have parsed a message with an unsupported type, and it cannot be
// very relevant for us. So we return an empty result without information to skip processing.
return null;
}
/**
* Attempt to decode the given message. If successful, the result is true and the reference
* parameters are set accordingly. Otherwise, false is returned and the reference parameters
* remain untouched.
*/
protected function tryDecodeMessage(
string $message,
?int &$command = null,
?int &$qualityOfService = null,
?bool &$retained = null,
?string &$data = null
): bool
{
// If we received no input, we can return immediately without doing work.
if (strlen($message) === 0) {
return false;
}
// If we received not at least the fixed header with one length indicating byte,
// we know that there can't be a valid message in the buffer. So we return early.
if (strlen($message) < 2) {
return false;
}
// Read the first byte of a message (command and flags).
$byte = $message[0];
$command = (int) (ord($byte) / 16);
$qualityOfService = (ord($byte) & 0x06) >> 1;
$retained = (bool) (ord($byte) & 0x01);
// Read the second byte of a message (remaining length).
// If the continuation bit (8) is set on the length byte, another byte will be read as length.
$byteIndex = 1;
$remainingLength = 0;
$multiplier = 1;
do {
// If the buffer has no more data, but we need to read more for the length header,
// we cannot give useful information about the remaining length and exit early.
if ($byteIndex + 1 > strlen($message)) {
return false;
}
// Otherwise, we can take seven bits to calculate the length and the remaining eighth bit
// as continuation bit.
$digit = ord($message[$byteIndex]);
$remainingLength += ($digit & 127) * $multiplier;
$multiplier *= 128;
$byteIndex++;
} while (($digit & 128) !== 0);
// At this point, we can now tell whether the remaining length amount of bytes are available
// or not. If not, the message is incomplete.
$requiredBytes = $byteIndex + $remainingLength;
if ($requiredBytes > strlen($message)) {
return false;
}
// Set the output data based on the calculated bytes.
$data = substr($message, $byteIndex, $remainingLength);
return true;
}
/**
* Parses a received published message. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [topic-length:topic:message]+
*/
protected function parseAndValidatePublishMessage(string $data, int $qualityOfServiceLevel, bool $retained): ?Message
{
$topicLength = (ord($data[0]) << 8) + ord($data[1]);
$topic = substr($data, 2, $topicLength);
$content = substr($data, ($topicLength + 2));
$message = new Message(MessageType::PUBLISH(), $qualityOfServiceLevel, $retained);
if ($qualityOfServiceLevel > self::QOS_AT_MOST_ONCE) {
if (strlen($content) < 2) {
$this->logger->error('Received a message with QoS level [{qos}] without message identifier. Waiting for retransmission.', [
'qos' => $qualityOfServiceLevel,
]);
// This message seems to be incomplete or damaged. We ignore it and wait for a retransmission,
// which will occur at some point due to QoS level > 0.
return null;
}
// Publish messages with a quality of service level > 0 require acknowledgement and therefore
// also a message identifier.
$messageId = $this->decodeMessageId($this->pop($content, 2));
$message->setMessageId($messageId);
}
return $message
->setTopic($topic)
->setContent($content);
}
/**
* Parses a received publish acknowledgement. The data contains the whole message except
* the fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidatePublishAcknowledgementMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid publish acknowledgement from the broker.');
throw new InvalidMessageException('Received invalid publish acknowledgement from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::PUBLISH_ACKNOWLEDGEMENT()))
->setMessageId($messageId);
}
/**
* Parses a received publish receipt. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidatePublishReceiptMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid publish receipt from the broker.');
throw new InvalidMessageException('Received invalid publish receipt from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::PUBLISH_RECEIPT()))
->setMessageId($messageId);
}
/**
* Parses a received publish release message. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidatePublishReleaseMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid publish release from the broker.');
throw new InvalidMessageException('Received invalid publish release from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::PUBLISH_RELEASE()))
->setMessageId($messageId);
}
/**
* Parses a received publish confirmation message. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidatePublishCompleteMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid publish complete from the broker.');
throw new InvalidMessageException('Received invalid complete release from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::PUBLISH_COMPLETE()))
->setMessageId($messageId);
}
/**
* Parses a received subscription acknowledgement. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier:[qos-level]+]
*
* The order of the received QoS levels matches the order of the sent subscriptions.
*
* @throws InvalidMessageException
*/
protected function parseAndValidateSubscribeAcknowledgementMessage(string $data): Message
{
if (strlen($data) < 3) {
$this->logger->notice('Received invalid subscribe acknowledgement from the broker.');
throw new InvalidMessageException('Received invalid subscribe acknowledgement from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
// Parse and validate the QoS acknowledgements.
$acknowledgements = array_map('ord', str_split($data));
foreach ($acknowledgements as $acknowledgement) {
if (!in_array($acknowledgement, [0, 1, 2])) {
throw new InvalidMessageException('Received subscribe acknowledgement with invalid QoS values from the broker.');
}
}
return (new Message(MessageType::SUBSCRIBE_ACKNOWLEDGEMENT()))
->setMessageId($messageId)
->setAcknowledgedQualityOfServices($acknowledgements);
}
/**
* Parses a received unsubscribe acknowledgement. The data contains the whole message except the
* fixed header with command and length. The message structure is:
*
* [message-identifier]
*
* @throws InvalidMessageException
*/
protected function parseAndValidateUnsubscribeAcknowledgementMessage(string $data): Message
{
if (strlen($data) !== 2) {
$this->logger->notice('Received invalid unsubscribe acknowledgement from the broker.');
throw new InvalidMessageException('Received invalid unsubscribe acknowledgement from the broker.');
}
$messageId = $this->decodeMessageId($this->pop($data, 2));
return (new Message(MessageType::UNSUBSCRIBE_ACKNOWLEDGEMENT()))
->setMessageId($messageId);
}
/**
* Parses a received ping request.
*/
protected function parseAndValidatePingRequestMessage(): Message
{
return new Message(MessageType::PING_REQUEST());
}
/**
* Parses a received ping acknowledgement.
*/
protected function parseAndValidatePingAcknowledgementMessage(): Message
{
return new Message(MessageType::PING_RESPONSE());
}
}

View File

@ -0,0 +1,37 @@
<?php
/** @noinspection PhpUnusedPrivateFieldInspection */
declare(strict_types=1);
namespace PhpMqtt\Client;
use MyCLabs\Enum\Enum;
/**
* An enumeration describing types of messages.
*
* @method static MessageType PUBLISH()
* @method static MessageType PUBLISH_ACKNOWLEDGEMENT()
* @method static MessageType PUBLISH_RECEIPT()
* @method static MessageType PUBLISH_RELEASE()
* @method static MessageType PUBLISH_COMPLETE()
* @method static MessageType SUBSCRIBE_ACKNOWLEDGEMENT()
* @method static MessageType UNSUBSCRIBE_ACKNOWLEDGEMENT()
* @method static MessageType PING_REQUEST()
* @method static MessageType PING_RESPONSE()
*
* @package PhpMqtt\Client
*/
class MessageType extends Enum
{
private const PUBLISH = 'PUBLISH';
private const PUBLISH_ACKNOWLEDGEMENT = 'PUBACK';
private const PUBLISH_RECEIPT = 'PUBREC';
private const PUBLISH_RELEASE = 'PUBREL';
private const PUBLISH_COMPLETE = 'PUBCOMP';
private const SUBSCRIBE_ACKNOWLEDGEMENT = 'SUBACK';
private const UNSUBSCRIBE_ACKNOWLEDGEMENT = 'UNSUBACK';
private const PING_REQUEST = 'PINGREQ';
private const PING_RESPONSE = 'PINGRESP';
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace PhpMqtt\Client;
use DateTime;
/**
* Represents a pending message.
*
* For messages with QoS 1 and 2 the client is responsible to resend the message if no
* acknowledgement is received from the broker within a given time period.
*
* This class serves as common base for message objects which need to be resent if no
* acknowledgement is received.
*
* @package PhpMqtt\Client
*/
abstract class PendingMessage
{
private int $sendingAttempts = 1;
private DateTime $lastSentAt;
/**
* Creates a new pending message object.
*/
protected function __construct(private int $messageId, ?DateTime $sentAt = null)
{
$this->lastSentAt = $sentAt ?? new DateTime();
}
/**
* Returns the message identifier.
*/
public function getMessageId(): int
{
return $this->messageId;
}
/**
* Returns the date time when the message was last sent.
*/
public function getLastSentAt(): DateTime
{
return $this->lastSentAt;
}
/**
* Returns the number of times the message has been sent.
*/
public function getSendingAttempts(): int
{
return $this->sendingAttempts;
}
/**
* Sets the date time when the message was last sent.
*/
public function setLastSentAt(?DateTime $value = null): self
{
$this->lastSentAt = $value ?? new DateTime();
return $this;
}
/**
* Increments the sending attempts by one.
*/
public function incrementSendingAttempts(): self
{
$this->sendingAttempts++;
return $this;
}
}

Some files were not shown because too many files have changed in this diff Show More