2020-02-09 23:02:19 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Api;
|
|
|
|
|
2022-02-20 16:53:26 +01:00
|
|
|
use Api\Parameter\Parameter;
|
2020-04-03 15:56:04 +02:00
|
|
|
use Objects\User;
|
2022-02-20 16:53:26 +01:00
|
|
|
use PhpMqtt\Client\MqttClient;
|
2020-04-03 15:56:04 +02:00
|
|
|
|
2020-02-09 23:02:19 +01:00
|
|
|
class Request {
|
|
|
|
|
2020-04-03 15:56:04 +02:00
|
|
|
protected User $user;
|
|
|
|
protected array $params;
|
|
|
|
protected string $lastError;
|
|
|
|
protected array $result;
|
|
|
|
protected bool $success;
|
|
|
|
protected bool $isPublic;
|
|
|
|
protected bool $loginRequired;
|
|
|
|
protected bool $variableParamCount;
|
|
|
|
protected bool $isDisabled;
|
|
|
|
protected bool $apiKeyAllowed;
|
2020-06-14 19:39:52 +02:00
|
|
|
protected bool $csrfTokenRequired;
|
2020-04-03 15:56:04 +02:00
|
|
|
|
2021-04-02 22:41:24 +02:00
|
|
|
private array $defaultParams;
|
2020-04-03 15:56:04 +02:00
|
|
|
private array $allowedMethods;
|
2020-04-03 18:09:01 +02:00
|
|
|
private bool $externalCall;
|
2020-04-03 15:56:04 +02:00
|
|
|
|
|
|
|
public function __construct(User $user, bool $externalCall = false, array $params = array()) {
|
2020-02-09 23:02:19 +01:00
|
|
|
$this->user = $user;
|
2021-04-02 22:41:24 +02:00
|
|
|
$this->defaultParams = $params;
|
2020-04-03 18:09:01 +02:00
|
|
|
|
2020-02-09 23:02:19 +01:00
|
|
|
$this->success = false;
|
|
|
|
$this->result = array();
|
2020-04-03 18:09:01 +02:00
|
|
|
$this->externalCall = $externalCall;
|
2020-02-09 23:02:19 +01:00
|
|
|
$this->isPublic = true;
|
|
|
|
$this->isDisabled = false;
|
|
|
|
$this->loginRequired = false;
|
|
|
|
$this->variableParamCount = false;
|
|
|
|
$this->apiKeyAllowed = true;
|
|
|
|
$this->allowedMethods = array("GET", "POST");
|
2020-04-03 18:09:01 +02:00
|
|
|
$this->lastError = "";
|
2020-06-23 15:31:09 +02:00
|
|
|
$this->csrfTokenRequired = true;
|
2020-02-09 23:02:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
protected function forbidMethod($method) {
|
|
|
|
if (($key = array_search($method, $this->allowedMethods)) !== false) {
|
2021-04-02 21:58:06 +02:00
|
|
|
unset($this->allowedMethods[$key]);
|
2020-02-09 23:02:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-20 16:53:26 +01:00
|
|
|
public function getDefaultParams(): array {
|
|
|
|
return $this->defaultParams;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function isDisabled(): bool {
|
|
|
|
return $this->isDisabled;
|
|
|
|
}
|
|
|
|
|
2021-12-08 16:53:43 +01:00
|
|
|
protected function allowMethod($method) {
|
|
|
|
$availableMethods = ["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "TRACE", "CONNECT"];
|
|
|
|
if (in_array($method, $availableMethods) && !in_array($method, $this->allowedMethods)) {
|
|
|
|
$this->allowedMethods[] = $method;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function getRequestMethod() {
|
|
|
|
return $_SERVER["REQUEST_METHOD"];
|
|
|
|
}
|
|
|
|
|
2021-11-11 14:25:26 +01:00
|
|
|
public function parseParams($values, $structure = NULL): bool {
|
2020-06-26 23:32:45 +02:00
|
|
|
|
2021-11-11 14:25:26 +01:00
|
|
|
if ($structure === NULL) {
|
|
|
|
$structure = $this->params;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($structure as $name => $param) {
|
2020-06-24 21:18:26 +02:00
|
|
|
$value = $values[$name] ?? NULL;
|
2020-02-09 23:02:19 +01:00
|
|
|
|
2021-01-13 01:36:04 +01:00
|
|
|
$isEmpty = (is_string($value) && strlen($value) === 0) || (is_array($value) && empty($value));
|
2021-04-02 21:58:06 +02:00
|
|
|
if (!$param->optional && (is_null($value) || $isEmpty)) {
|
2020-06-27 22:47:12 +02:00
|
|
|
return $this->createError("Missing parameter: $name");
|
2020-02-09 23:02:19 +01:00
|
|
|
}
|
|
|
|
|
2022-02-20 16:53:26 +01:00
|
|
|
$param->reset();
|
2021-04-02 21:58:06 +02:00
|
|
|
if (!is_null($value) && !$isEmpty) {
|
|
|
|
if (!$param->parseParam($value)) {
|
2020-02-09 23:02:19 +01:00
|
|
|
$value = print_r($value, true);
|
2020-06-27 22:47:12 +02:00
|
|
|
return $this->createError("Invalid Type for parameter: $name '$value' (Required: " . $param->getTypeName() . ")");
|
2020-02-09 23:02:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-04-02 21:58:06 +02:00
|
|
|
|
2020-02-09 23:02:19 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-02-10 00:52:25 +01:00
|
|
|
public function parseVariableParams($values) {
|
2021-04-02 21:58:06 +02:00
|
|
|
foreach ($values as $name => $value) {
|
|
|
|
if (isset($this->params[$name])) continue;
|
2020-02-09 23:02:19 +01:00
|
|
|
$type = Parameter\Parameter::parseType($value);
|
|
|
|
$param = new Parameter\Parameter($name, $type, true);
|
|
|
|
$param->parseParam($value);
|
|
|
|
$this->params[$name] = $param;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-08 16:53:43 +01:00
|
|
|
// wrapper for unit tests
|
|
|
|
protected function _die(string $data = ""): bool {
|
|
|
|
die($data);
|
|
|
|
}
|
|
|
|
|
2021-04-02 21:58:06 +02:00
|
|
|
public function execute($values = array()): bool {
|
2022-02-20 16:53:26 +01:00
|
|
|
|
2021-04-02 22:42:53 +02:00
|
|
|
$this->params = array_merge([], $this->defaultParams);
|
2020-02-09 23:02:19 +01:00
|
|
|
$this->success = false;
|
|
|
|
$this->result = array();
|
|
|
|
$this->lastError = '';
|
|
|
|
|
2021-04-02 21:58:06 +02:00
|
|
|
if ($this->user->isLoggedIn()) {
|
2020-02-09 23:02:19 +01:00
|
|
|
$this->result['logoutIn'] = $this->user->getSession()->getExpiresSeconds();
|
|
|
|
}
|
|
|
|
|
2021-04-02 21:58:06 +02:00
|
|
|
if ($this->externalCall) {
|
2020-02-10 00:52:25 +01:00
|
|
|
$values = $_REQUEST;
|
2021-04-02 21:58:06 +02:00
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_SERVER["CONTENT_TYPE"]) && in_array("application/json", explode(";", $_SERVER["CONTENT_TYPE"]))) {
|
2020-02-09 23:02:19 +01:00
|
|
|
$jsonData = json_decode(file_get_contents('php://input'), true);
|
2021-11-11 14:25:26 +01:00
|
|
|
if ($jsonData !== null) {
|
2020-06-14 22:35:01 +02:00
|
|
|
$values = array_merge($values, $jsonData);
|
|
|
|
} else {
|
|
|
|
$this->lastError = 'Invalid request body.';
|
2021-12-08 16:53:43 +01:00
|
|
|
http_response_code(400);
|
2020-06-14 22:35:01 +02:00
|
|
|
return false;
|
|
|
|
}
|
2020-02-09 23:02:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-02 21:58:06 +02:00
|
|
|
if ($this->isDisabled) {
|
2020-02-09 23:02:19 +01:00
|
|
|
$this->lastError = "This function is currently disabled.";
|
2021-12-08 16:53:43 +01:00
|
|
|
http_response_code(503);
|
2020-02-09 23:02:19 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-04-02 21:58:06 +02:00
|
|
|
if ($this->externalCall && !$this->isPublic) {
|
2020-02-09 23:02:19 +01:00
|
|
|
$this->lastError = 'This function is private.';
|
2021-12-08 16:53:43 +01:00
|
|
|
http_response_code(403);
|
2020-02-09 23:02:19 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-04-02 21:58:06 +02:00
|
|
|
if ($this->externalCall) {
|
2021-04-07 00:03:14 +02:00
|
|
|
|
2021-12-08 16:53:43 +01:00
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
|
|
http_response_code(204); # No content
|
|
|
|
header("Allow: OPTIONS, " . implode(", ", $this->allowedMethods));
|
|
|
|
return $this->_die();
|
|
|
|
}
|
|
|
|
|
2021-04-07 00:03:14 +02:00
|
|
|
// check the request method
|
|
|
|
if (!in_array($_SERVER['REQUEST_METHOD'], $this->allowedMethods)) {
|
|
|
|
$this->lastError = 'This method is not allowed';
|
2021-12-08 16:53:43 +01:00
|
|
|
http_response_code(405);
|
2021-04-07 00:03:14 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-06-14 19:39:52 +02:00
|
|
|
$apiKeyAuthorized = false;
|
2020-06-27 01:18:10 +02:00
|
|
|
|
2021-12-08 16:53:43 +01:00
|
|
|
if (!$this->user->isLoggedIn() && $this->apiKeyAllowed) {
|
|
|
|
if (isset($_SERVER["HTTP_AUTHORIZATION"])) {
|
2021-11-11 14:25:26 +01:00
|
|
|
$authHeader = $_SERVER["HTTP_AUTHORIZATION"];
|
|
|
|
if (startsWith($authHeader, "Bearer ")) {
|
|
|
|
$apiKey = substr($authHeader, strlen("Bearer "));
|
|
|
|
$apiKeyAuthorized = $this->user->authorize($apiKey);
|
|
|
|
}
|
2020-06-27 01:18:10 +02:00
|
|
|
}
|
2021-12-08 16:53:43 +01:00
|
|
|
}
|
2020-06-27 01:18:10 +02:00
|
|
|
|
2021-12-08 16:53:43 +01:00
|
|
|
// Logged in or api key authorized?
|
|
|
|
if ($this->loginRequired) {
|
2021-04-02 21:58:06 +02:00
|
|
|
if (!$this->user->isLoggedIn() && !$apiKeyAuthorized) {
|
2020-06-27 01:18:10 +02:00
|
|
|
$this->lastError = 'You are not logged in.';
|
2021-12-08 16:53:43 +01:00
|
|
|
http_response_code(401);
|
2020-06-27 01:18:10 +02:00
|
|
|
return false;
|
2022-02-20 16:53:26 +01:00
|
|
|
} else if ($this->user->isLoggedIn()) {
|
|
|
|
$tfaToken = $this->user->getTwoFactorToken();
|
|
|
|
if ($tfaToken && $tfaToken->isConfirmed() && !$tfaToken->isAuthenticated()) {
|
|
|
|
$this->lastError = '2FA-Authorization is required';
|
|
|
|
http_response_code(401);
|
|
|
|
return false;
|
|
|
|
}
|
2020-06-27 01:18:10 +02:00
|
|
|
}
|
2020-06-27 22:47:12 +02:00
|
|
|
}
|
2020-02-09 23:02:19 +01:00
|
|
|
|
2020-06-27 22:47:12 +02:00
|
|
|
// CSRF Token
|
2021-04-02 21:58:06 +02:00
|
|
|
if ($this->csrfTokenRequired && $this->user->isLoggedIn()) {
|
2020-06-27 22:47:12 +02:00
|
|
|
// csrf token required + external call
|
|
|
|
// if it's not a call with API_KEY, check for csrf_token
|
2022-02-20 16:53:26 +01:00
|
|
|
$csrfToken = $values["csrf_token"] ?? $_SERVER["HTTP_XSRF_TOKEN"] ?? null;
|
|
|
|
if (!$csrfToken || strcmp($csrfToken, $this->user->getSession()->getCsrfToken()) !== 0) {
|
2020-06-27 22:47:12 +02:00
|
|
|
$this->lastError = "CSRF-Token mismatch";
|
2021-12-08 16:53:43 +01:00
|
|
|
http_response_code(403);
|
2020-06-27 22:47:12 +02:00
|
|
|
return false;
|
2020-06-14 19:39:52 +02:00
|
|
|
}
|
2020-02-09 23:02:19 +01:00
|
|
|
}
|
2020-06-27 01:18:10 +02:00
|
|
|
|
|
|
|
// Check for permission
|
2020-06-27 22:47:12 +02:00
|
|
|
if (!($this instanceof \Api\Permission\Save)) {
|
2020-06-27 01:18:10 +02:00
|
|
|
$req = new \Api\Permission\Check($this->user);
|
|
|
|
$this->success = $req->execute(array("method" => $this->getMethod()));
|
|
|
|
$this->lastError = $req->getLastError();
|
|
|
|
if (!$this->success) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2020-02-09 23:02:19 +01:00
|
|
|
}
|
|
|
|
|
2021-04-02 21:58:06 +02:00
|
|
|
if (!$this->parseParams($values)) {
|
2020-02-09 23:02:19 +01:00
|
|
|
return false;
|
2021-04-02 21:58:06 +02:00
|
|
|
}
|
2020-02-09 23:02:19 +01:00
|
|
|
|
2021-04-02 21:58:06 +02:00
|
|
|
if ($this->variableParamCount) {
|
2020-02-10 00:52:25 +01:00
|
|
|
$this->parseVariableParams($values);
|
2021-04-02 21:58:06 +02:00
|
|
|
}
|
2020-02-09 23:02:19 +01:00
|
|
|
|
2021-04-02 21:58:06 +02:00
|
|
|
if (!$this->user->getSQL()->isConnected()) {
|
2020-02-09 23:02:19 +01:00
|
|
|
$this->lastError = $this->user->getSQL()->getLastError();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->user->getSQL()->setLastError('');
|
|
|
|
$this->success = true;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-04-02 21:58:06 +02:00
|
|
|
protected function createError($err): bool {
|
2020-02-09 23:02:19 +01:00
|
|
|
$this->success = false;
|
|
|
|
$this->lastError = $err;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-11-11 14:25:26 +01:00
|
|
|
protected function getParam($name, $obj = NULL) {
|
2021-04-02 22:41:24 +02:00
|
|
|
// i don't know why phpstorm
|
2021-11-11 14:25:26 +01:00
|
|
|
if ($obj === NULL) {
|
|
|
|
$obj = $this->params;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (isset($obj[$name]) ? $obj[$name]->value : NULL);
|
2021-04-02 21:58:06 +02:00
|
|
|
}
|
|
|
|
|
2022-02-20 16:53:26 +01:00
|
|
|
public function isMethodAllowed(string $method): bool {
|
|
|
|
return in_array($method, $this->allowedMethods);
|
|
|
|
}
|
|
|
|
|
2021-04-02 21:58:06 +02:00
|
|
|
public function isPublic(): bool {
|
|
|
|
return $this->isPublic;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getLastError(): string {
|
|
|
|
return $this->lastError;
|
2020-06-23 15:31:09 +02:00
|
|
|
}
|
2020-04-03 15:56:04 +02:00
|
|
|
|
2021-04-02 21:58:06 +02:00
|
|
|
public function getResult(): array {
|
|
|
|
return $this->result;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function success(): bool {
|
|
|
|
return $this->success;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function loginRequired(): bool {
|
|
|
|
return $this->loginRequired;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function isExternalCall(): bool {
|
|
|
|
return $this->externalCall;
|
|
|
|
}
|
2020-02-09 23:02:19 +01:00
|
|
|
|
2020-06-27 01:18:10 +02:00
|
|
|
private function getMethod() {
|
|
|
|
$class = str_replace("\\", "/", get_class($this));
|
|
|
|
$class = substr($class, strlen("api/"));
|
|
|
|
return $class;
|
|
|
|
}
|
|
|
|
|
2021-04-02 21:58:06 +02:00
|
|
|
public function getJsonResult(): string {
|
2020-02-09 23:02:19 +01:00
|
|
|
$this->result['success'] = $this->success;
|
|
|
|
$this->result['msg'] = $this->lastError;
|
|
|
|
return json_encode($this->result);
|
|
|
|
}
|
2021-11-11 14:25:26 +01:00
|
|
|
|
|
|
|
protected function disableOutputBuffer() {
|
|
|
|
ob_implicit_flush(true);
|
|
|
|
$levels = ob_get_level();
|
|
|
|
for ( $i = 0; $i < $levels; $i ++ ) {
|
|
|
|
ob_end_flush();
|
|
|
|
}
|
|
|
|
flush();
|
|
|
|
}
|
2021-12-08 16:53:43 +01:00
|
|
|
|
2022-02-20 16:53:26 +01:00
|
|
|
protected function disableCache() {
|
|
|
|
header("Last-Modified: " . (new \DateTime())->format("D, d M Y H:i:s T"));
|
|
|
|
header("Expires: Sat, 26 Jul 1997 05:00:00 GMT");
|
|
|
|
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
|
|
|
|
header("Cache-Control: post-check=0, pre-check=0", false);
|
|
|
|
header("Pragma: no-cache");
|
|
|
|
}
|
|
|
|
|
2021-12-08 16:53:43 +01:00
|
|
|
protected function setupSSE() {
|
|
|
|
$this->user->getSQL()->close();
|
|
|
|
$this->user->sendCookies();
|
|
|
|
set_time_limit(0);
|
|
|
|
ignore_user_abort(true);
|
|
|
|
header('Content-Type: text/event-stream');
|
|
|
|
header('Connection: keep-alive');
|
|
|
|
header('X-Accel-Buffering: no');
|
2022-02-20 16:53:26 +01:00
|
|
|
$this->disableCache();
|
2021-12-08 16:53:43 +01:00
|
|
|
$this->disableOutputBuffer();
|
|
|
|
}
|
|
|
|
|
2022-02-20 16:53:26 +01:00
|
|
|
/**
|
|
|
|
* @throws \PhpMqtt\Client\Exceptions\ProtocolViolationException
|
|
|
|
* @throws \PhpMqtt\Client\Exceptions\DataTransferException
|
|
|
|
* @throws \PhpMqtt\Client\Exceptions\MqttClientException
|
|
|
|
*/
|
|
|
|
protected function startMqttSSE(MqttClient $mqtt, callable $onPing) {
|
|
|
|
$lastPing = 0;
|
|
|
|
$mqtt->registerLoopEventHandler(function(MqttClient $mqtt, $elapsed) use (&$lastPing, $onPing) {
|
|
|
|
if ($elapsed - $lastPing >= 5) {
|
|
|
|
$onPing();
|
|
|
|
$lastPing = $elapsed;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (connection_status() !== 0) {
|
|
|
|
$mqtt->interrupt();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
$mqtt->loop();
|
|
|
|
$this->lastError = "MQTT Loop disconnected";
|
|
|
|
$mqtt->disconnect();
|
|
|
|
}
|
|
|
|
|
2021-12-08 16:53:43 +01:00
|
|
|
protected function processImageUpload(string $uploadDir, array $allowedExtensions = ["jpg","jpeg","png","gif"], $transformCallback = null) {
|
|
|
|
if (empty($_FILES)) {
|
|
|
|
return $this->createError("You need to upload an image.");
|
|
|
|
} else if (count($_FILES) > 1) {
|
|
|
|
return $this->createError("You can only upload one image at once.");
|
|
|
|
}
|
|
|
|
|
|
|
|
$upload = array_values($_FILES)[0];
|
|
|
|
if (is_array($upload["name"])) {
|
|
|
|
return $this->createError("You can only upload one image at once.");
|
|
|
|
} else if ($upload["error"] !== UPLOAD_ERR_OK) {
|
|
|
|
return $this->createError("There was an error uploading the image, code: " . $upload["error"]);
|
|
|
|
}
|
|
|
|
|
|
|
|
$imageName = $upload["name"];
|
|
|
|
$ext = strtolower(pathinfo($imageName, PATHINFO_EXTENSION));
|
|
|
|
if (!in_array($ext, $allowedExtensions)) {
|
|
|
|
return $this->createError("Only the following file extensions are allowed: " . implode(",", $allowedExtensions));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_dir($uploadDir) && !mkdir($uploadDir, 0777, true)) {
|
|
|
|
return $this->createError("Upload directory does not exist and could not be created.");
|
|
|
|
}
|
|
|
|
|
|
|
|
$srcPath = $upload["tmp_name"];
|
|
|
|
$mimeType = mime_content_type($srcPath);
|
|
|
|
if (!startsWith($mimeType, "image/")) {
|
|
|
|
return $this->createError("Uploaded file is not an image.");
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
$image = new \Imagick($srcPath);
|
|
|
|
|
|
|
|
// strip exif
|
|
|
|
$profiles = $image->getImageProfiles("icc", true);
|
|
|
|
$image->stripImage();
|
|
|
|
if (!empty($profiles)) {
|
|
|
|
$image->profileImage("icc", $profiles['icc']);
|
|
|
|
}
|
|
|
|
} catch (\ImagickException $ex) {
|
|
|
|
return $this->createError("Error loading image: " . $ex->getMessage());
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
if ($transformCallback) {
|
|
|
|
$fileName = call_user_func([$this, $transformCallback], $image, $uploadDir);
|
|
|
|
} else {
|
|
|
|
|
|
|
|
$image->writeImage($srcPath);
|
|
|
|
$image->destroy();
|
|
|
|
|
|
|
|
$uuid = uuidv4();
|
|
|
|
$fileName = "$uuid.$ext";
|
|
|
|
$destPath = "$uploadDir/$fileName";
|
|
|
|
if (!file_exists($destPath)) {
|
|
|
|
if (!@move_uploaded_file($srcPath, $destPath)) {
|
|
|
|
return $this->createError("Could not store uploaded file.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return [$fileName, $imageName];
|
|
|
|
} catch (\ImagickException $ex) {
|
|
|
|
return $this->createError("Error processing image: " . $ex->getMessage());
|
|
|
|
}
|
|
|
|
}
|
2020-04-03 15:56:04 +02:00
|
|
|
}
|