feat(cloudron): add tirreno package artifacts

- Add CloudronStack/output/CloudronPackages-Artifacts/tirreno/ directory and its contents
- Includes package manifest, Dockerfile, source code, documentation, and build artifacts
- Add tirreno-1761840148.tar.gz as a build artifact
- Add tirreno-cloudron-package-1761841304.tar.gz as the Cloudron package
- Include all necessary files for the tirreno Cloudron package

This adds the complete tirreno Cloudron package artifacts to the repository.
This commit is contained in:
2025-10-30 11:43:06 -05:00
parent 0ce353ea9d
commit 91d52d2de5
1692 changed files with 202851 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
use Sensor\Model\Http\RegularResponse;
use Sensor\Model\Http\Request;
use Sensor\Service\DI;
ini_set('display_errors', '0');
if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
require __DIR__ . '/../vendor/autoload.php';
} else {
require __DIR__ . '/../libs/mustangostang/spyc/Spyc.php';
require __DIR__ . '/../libs/matomo/device-detector/autoload.php';
}
// Register autoloader
spl_autoload_register(fn (string $c) => @include_once __DIR__ . '/src/' . str_replace(['Sensor\\', '\\'], ['', '/'], $c) . '.php');
$requestStartTime = new \DateTime('now');
$di = new DI();
$profiler = $di->getProfiler();
$logger = $di->getLogger();
$logbookManager = $di->getLogbookManager();
$profiler->start('total');
$request = null;
try {
$apiKeyString = $_SERVER['HTTP_API_KEY'] ?? null;
$apiKeyDto = $logbookManager->getApiKeyDto($apiKeyString); // GetApiKeyDto or null
$logbookManager->setApiKeyDto($apiKeyDto);
$request = new Request($_POST, $apiKeyString, $_SERVER['HTTP_X_REQUEST_ID'] ?? null);
$controller = $di->getController();
$response = $controller->index($request, $apiKeyDto);
} catch (Throwable $e) {
if ($e instanceof PDOException && str_contains($e->getMessage(), 'connect')) {
$logger->logError($e, 'Unable to connect to database: ' . $e->getMessage());
} else {
$logger->logError($e);
}
// get apikey
$logbookManager->logException(
$requestStartTime,
$e->getMessage(),
);
$logbookManager->logIncorrectRequest(
$request?->body ?? [],
$e::class . ': ' . $e->getMessage(),
$request?->traceId ?? null,
);
// Log profiler data and queries before exit
$profiler->finish('total');
$logger->logProfilerData($profiler->getData());
http_response_code(500);
exit;
}
$profiler->finish('total');
$logger->logProfilerData($profiler->getData());
// getapikey
$logbookManager->logRequest($requestStartTime, $response);
// response without errors
if ($response instanceof RegularResponse) {
return;
}
// Response is set only in case of error, so let's log it
$logger->logUserError($response->httpCode, (string) $response);
// getapikey
$logbookManager->logIncorrectRequest(
$request?->body ?? [],
(string) $response,
$request?->traceId ?? null,
);

View File

@@ -0,0 +1,155 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Controller;
use Sensor\Dto\GetApiKeyDto;
use Sensor\Dto\InsertAccountDto;
use Sensor\Dto\InsertEventDto;
use Sensor\Entity\AccountEntity;
use Sensor\Exception\ValidationException;
use Sensor\Factory\EventFactory;
use Sensor\Factory\RequestFactory;
use Sensor\Model\CreateEventDto;
use Sensor\Model\Http\RegularResponse;
use Sensor\Model\Http\ErrorResponse;
use Sensor\Model\Http\Request;
use Sensor\Model\Http\ValidationFailedResponse;
use Sensor\Repository\AccountRepository;
use Sensor\Repository\ApiKeyRepository;
use Sensor\Repository\EventRepository;
use Sensor\Service\ConnectionService;
use Sensor\Service\Enrichment\DataEnrichmentService;
use Sensor\Service\DeviceDetectorService;
use Sensor\Service\FraudDetectionService;
use Sensor\Service\Logger;
use Sensor\Service\Profiler;
use Sensor\Service\QueryParser;
class CreateEventController {
public function __construct(
private RequestFactory $requestFactory,
private EventFactory $eventFactory,
private ConnectionService $connectionService,
private QueryParser $queryParser,
private ?DataEnrichmentService $dataEnrichmentService,
private DeviceDetectorService $deviceDetectorService,
private FraudDetectionService $fraudDetectionService,
private ApiKeyRepository $apiKeyRepository,
private EventRepository $eventRepository,
private AccountRepository $accountRepository,
private \PDO $pdo,
private Profiler $profiler,
private Logger $logger,
) {
}
public function index(Request $request, ?GetApiKeyDto $apiKeyDto): RegularResponse|ErrorResponse {
$this->logger->logDebug('Request: ' . json_encode($request->body));
$this->profiler->start('user');
try {
$dto = $this->requestFactory->createFromArray($request->body, $request->traceId, $this->eventRepository);
} catch (ValidationException $e) {
return new ValidationFailedResponse($e);
}
if ($request->apiKey === null) {
return new ErrorResponse('Api-Key header is not set', 401);
}
if ($apiKeyDto === null) {
return new ErrorResponse('API key from the "Api-Key" header is not found', 401);
}
$this->connectionService->finishRequestForUser();
$this->profiler->finish('user');
// Get account ID
$account = new AccountEntity(
$apiKeyDto->id,
$dto->userName,
$dto->ipAddress->value,
$dto->fullName,
$dto->firstName,
$dto->lastName,
$dto->eventTime,
$dto->userCreated,
);
$accountDto = $this->accountRepository->checkExistence($account);
if ($this->dataEnrichmentService === null) {
$this->logger->logDebug('Skipping calling enrichment API, because it\'s not configured');
}
$enrichedData = $this->dataEnrichmentService?->getEnrichmentData(
$apiKeyDto,
$accountDto?->accountId,
$dto->ipAddress, // check localhost inside of getEnrichmentData()
$dto->emailAddress,
$dto->emailDomain,
$dto->phoneNumber,
);
// Check if ip/email/phone is blacklisted
$fraudDetected = $this->fraudDetectionService->getEarlierDetectedFraud(
$apiKeyDto->id,
$dto->emailAddress?->value,
$dto->ipAddress->value,
$dto->phoneNumber?->value,
);
$deviceDetected = $this->deviceDetectorService->parse($apiKeyDto, $dto->userAgent);
$query = $this->queryParser->getQueryFromUrl($dto->url);
// Insert account and event into single transaction
$this->pdo->beginTransaction();
try {
$this->apiKeyRepository->updateApiCallReached($enrichedData?->reached, $apiKeyDto);
$accountDto = $this->accountRepository->insert($account);
$event = $this->eventFactory->createFromDto(
$accountDto->accountId,
$accountDto->sessionId,
$apiKeyDto->id,
$dto,
$enrichedData,
$fraudDetected,
$deviceDetected,
$query,
);
$this->profiler->start('db_insert');
$insertDto = $this->eventRepository->insert(
$event,
$accountDto->lastEmailId,
$accountDto->lastPhoneId,
);
$this->profiler->finish('db_insert');
$this->pdo->commit();
} catch (\Throwable $e) {
$this->pdo->rollBack();
throw $e;
}
return new RegularResponse($insertDto->eventId, $dto->changedParams);
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Dto;
class GetApiKeyDto {
public function __construct(
public int $id,
public string $key,
public ?string $token,
public bool $hashExchange,
public bool $skipEnrichingDomains,
public bool $skipEnrichingEmails,
public bool $skipEnrichingIps,
public bool $skipEnrichingUserAgents,
public bool $skipEnrichingPhones,
) {
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Dto;
class InsertAccountDto {
public function __construct(
public int $accountId,
public ?int $lastEmailId,
public ?int $lastPhoneId,
public int $sessionId
) {
}
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Dto;
class InsertCountryDto {
public function __construct(
public int $eventCountryId,
) {
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Dto;
class InsertEmailDto {
public function __construct(
public int $emailId,
public int $domainId,
) {
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Dto;
class InsertEventDto {
public function __construct(
public int $eventId,
public int $ipAddressId,
public int $urlId,
public int $deviceId,
public int $countryId,
public ?int $domainId,
public ?int $ispId,
public ?int $payloadId,
) {
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Dto;
class InsertIpAddressDto {
public function __construct(
public int $ipAddressId,
public int $countryId,
public ?int $ispId,
) {
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Dto;
class InsertUrlDto {
public function __construct(
public int $urlId,
public ?int $queryId,
) {
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Dto;
class InsertUserAgentDto {
public function __construct(
public int $userAgentId,
public ?string $osName,
public ?string $osVersion,
public ?string $browserName,
public ?string $browserVersion,
public ?string $device,
) {
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class AccountEntity {
public function __construct(
public int $apiKeyId,
public string $userName,
public string $lastIp,
public ?string $fullName,
public ?string $firstName,
public ?string $lastName,
public \DateTimeImmutable $lastSeen,
public ?\DateTimeImmutable $userCreated,
) {
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class CountryEntity {
public function __construct(
public int $apiKeyId,
public int $countryId,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class DeviceEntity {
public function __construct(
public int $accountId,
public int $apiKeyId,
public UserAgentEntity|UserAgentEnrichedEntity|null $userAgent,
public ?string $lang,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class DomainEnrichedEntity {
public function __construct(
public int $apiKeyId,
public string $domain,
public bool $blockdomains,
public bool $disposableDomains,
public bool $freeEmailProvider,
public ?string $ip,
public ?string $geoIp,
public ?string $geoHtml,
public ?string $webServer,
public ?string $hostname,
public ?string $emails,
public ?string $phone,
public string $discoveryDate,
public ?int $trancoRank,
public ?string $creationDate,
public ?string $expirationDate,
public ?int $returnCode,
public bool $disabled,
public ?string $closestSnapshot,
public bool $mxRecord,
public bool $checked,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class DomainEntity {
public function __construct(
public int $apiKeyId,
public string $domain,
public ?bool $checked, // null if was not sent to enrichment
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class DomainNotFoundEntity {
public function __construct(
public int $apiKeyId,
public string $domain,
public bool $blockdomains,
public bool $disposableDomains,
public bool $freeEmailProvider,
public ?string $creationDate,
public ?string $expirationDate,
public ?int $returnCode,
public bool $disabled,
public ?string $closestSnapshot,
public bool $mxRecord,
public bool $checked,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class EmailEnrichedEntity {
public function __construct(
public int $accountId,
public int $apiKeyId,
public string $email,
public ?string $hash,
public DomainEntity|DomainEnrichedEntity|DomainNotFoundEntity $domain,
public bool $blockEmails,
public bool $dataBreach,
public int $dataBreaches,
public ?\DateTimeImmutable $earliestBreach,
public int $profiles,
public bool $domainContactEmail,
public ?bool $alertList,
public bool $fraudDetected,
public bool $checked,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class EmailEntity {
public function __construct(
public int $accountId,
public int $apiKeyId,
public string $email,
public ?string $hash,
public DomainEntity|DomainEnrichedEntity|DomainNotFoundEntity $domain,
public bool $fraudDetected,
public ?bool $checked, // null if was not sent to enrichment
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class EventEntity {
/**
* @param array<mixed, mixed>|null $payload
*/
public function __construct(
public int $accountId,
public SessionEntity $session,
public int $apiKeyId,
public IpAddressEntity|IpAddressEnrichedEntity|IpAddressLocalhostEnrichedEntity $ipAddress,
public UrlEntity $url,
public ?string $eventType,
public ?string $httpMethod,
public DeviceEntity $device,
public ?RefererEntity $referer,
public EmailEntity|EmailEnrichedEntity|null $email,
public PhoneEntity|PhoneEnrichedEntity|PhoneInvalidEntity|null $phone,
public ?int $httpCode,
public \DateTimeImmutable $eventTime,
public ?string $traceId,
public ?PayloadEntity $payload,
public CountryEntity $country,
) {
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class IpAddressEnrichedEntity {
/**
* @param string[] $domainsCount
*/
public function __construct(
public int $apiKeyId,
public string $ipAddress,
public ?string $hash,
public int $countryId,
public bool $hosting,
public bool $vpn,
public bool $tor,
public bool $relay,
public bool $starlink,
public bool $blocklist,
public array $domainsCount,
public ?string $cidr,
public ?bool $alertList,
public bool $fraudDetected,
public ?IspEnrichedEntity $isp,
public bool $checked,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class IpAddressEntity {
public function __construct(
public int $apiKeyId,
public string $ipAddress,
public ?string $hash,
public bool $fraudDetected,
public IspEntity|IspLocalhostEntity|IspEnrichedEntity $isp,
public ?bool $checked, // null if was not sent to enrichment
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class IpAddressLocalhostEnrichedEntity {
/**
* @param string[] $domainsCount
*/
public function __construct(
public int $apiKeyId,
public string $ipAddress,
public ?string $hash,
public bool $fraudDetected,
public ?IspLocalhostEntity $isp,
public \DateTimeImmutable $lastSeen,
public int $countryId = 0,
public bool $hosting = false,
public bool $vpn = false,
public bool $tor = false,
public bool $relay = false,
public bool $starlink = false,
public bool $blocklist = false,
public array $domainsCount = [],
public ?string $cidr = null,
public ?bool $alertList = null,
public bool $checked = true,
) {
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class IspEnrichedEntity {
public function __construct(
public int $apiKeyId,
public int $asn,
public ?string $name,
public ?string $description,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class IspEntity {
public function __construct(
public int $apiKeyId,
public \DateTimeImmutable $lastSeen,
public int $asn = 64496,
public string $name = 'N/A',
public ?string $description = null,
) {
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class IspLocalhostEntity {
public function __construct(
public int $apiKeyId,
public \DateTimeImmutable $lastSeen,
public int $asn = 0,
public string $name = 'Local area network',
public ?string $description = null,
) {
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class LogbookEntity {
public const ERROR_TYPE_SUCCESS = 0;
public const ERROR_TYPE_VALIDATION_ERROR = 1;
public const ERROR_TYPE_CRITICAL_VALIDATION_ERROR = 2;
public const ERROR_TYPE_CRITICAL_ERROR = 3;
public function __construct(
public int $apiKeyId,
public string $ip,
public ?int $eventId,
public int $errorType,
public ?string $errorText,
public string $raw,
public ?string $started,
) {
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class PayloadEntity {
public function __construct(
public int $accountId,
public int $apiKeyId,
public array|string $payload,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class PhoneEnrichedEntity {
public function __construct(
public int $accountId,
public int $apiKeyId,
public string $phoneNumber,
public ?string $hash,
public int $profiles,
public ?int $countryId,
public int $callingCountryCode,
public string $nationalFormat,
public bool $invalid,
public ?string $validationErrors,
public ?string $carrierName,
public string $type,
public ?bool $alertList,
public bool $fraudDetected,
public bool $checked,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class PhoneEntity {
public function __construct(
public int $accountId,
public int $apiKeyId,
public string $phoneNumber,
public ?string $hash,
public ?int $countryId,
public bool $fraudDetected,
public ?bool $checked, // null if was not sent to enrichment
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class PhoneInvalidEntity {
public function __construct(
public int $accountId,
public int $apiKeyId,
public string $phoneNumber,
public ?string $hash,
public int $countryId,
public bool $fraudDetected,
public string $validationErrors,
public bool $checked,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class RefererEntity {
public function __construct(
public int $apiKeyId,
public string $referer,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class SessionEntity {
public function __construct(
public int $id,
public int $accountId,
public int $apiKeyId,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class UrlEntity {
public function __construct(
public int $apiKeyId,
public string $url,
public ?UrlQueryEntity $query,
public ?string $title,
public ?int $httpCode,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class UrlQueryEntity {
public function __construct(
public int $apiKeyId,
public string $query,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class UserAgentEnrichedEntity {
public function __construct(
public int $apiKeyId,
public string $userAgent,
public ?string $device,
public ?string $browserName,
public ?string $browserVersion,
public ?string $osName,
public ?string $osVersion,
public bool $modified,
public bool $checked,
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,28 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Entity;
class UserAgentEntity {
public function __construct(
public int $apiKeyId,
public ?string $userAgent,
public ?bool $checked, // null if was not sent to enrichment
public \DateTimeImmutable $lastSeen,
) {
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Exception;
class AuthException extends \RuntimeException {
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Exception;
class ForbiddenException extends \RuntimeException {
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Exception;
class RateLimitException extends \RuntimeException {
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Exception;
class ValidationException extends \RuntimeException {
public function __construct(
string $message,
private string $key,
) {
parent::__construct($message);
}
public function getKey(): string {
return $this->key;
}
}

View File

@@ -0,0 +1,215 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Factory;
use Sensor\Model\Enriched\EnrichedData;
use Sensor\Model\Enriched\DomainEnriched;
use Sensor\Model\Enriched\DomainNotFoundEnriched;
use Sensor\Model\Enriched\DomainEnrichFailed;
use Sensor\Model\Enriched\EmailEnriched;
use Sensor\Model\Enriched\EmailEnrichFailed;
use Sensor\Model\Enriched\IpAddressEnriched;
use Sensor\Model\Enriched\IpAddressLocalhostEnriched;
use Sensor\Model\Enriched\IpAddressEnrichFailed;
use Sensor\Model\Enriched\IspEnriched;
use Sensor\Model\Enriched\IspEnrichedEmpty;
use Sensor\Model\Enriched\IspEnrichedLocalhost;
use Sensor\Model\Enriched\PhoneEnriched;
use Sensor\Model\Enriched\PhoneInvalidEnriched;
use Sensor\Model\Enriched\PhoneEnrichFailed;
use Sensor\Service\Enrichment\DataEnrichmentClientInterface;
use Sensor\Service\Logger;
/**
* @phpstan-import-type EnrichmentClientResponse from DataEnrichmentClientInterface
*/
class EnrichedDataFactory {
private const IP_IS_BOGON_ERROR_TEXT = 'IP is bogon';
private const VALIDATION_ERROR_TEXT = 'Validation failed';
private const SERVER_ERROR = 'Server Error';
public function __construct(
private Logger $logger,
) {
}
/**
* @phpstan-param EnrichmentClientResponse $data
*/
public function createFromResponse(array $data, array $origin): EnrichedData {
$email = null;
if (isset($data['email']['email'])) {
try {
$earliestBreach = $data['email']['earliest_breach'] !== null ? new \DateTimeImmutable($data['email']['earliest_breach']) : null;
$email = new EmailEnriched(
$data['email']['email'],
$data['email']['blockemails'],
$data['email']['data_breach'],
$data['email']['data_breaches'],
$earliestBreach,
$data['email']['profiles'],
$data['email']['domain_contact_email'],
$data['email']['alert_list'],
);
} catch (\Throwable $e) {
$this->logger->logWarning('Error during parsing email response', $e);
}
} elseif (isset($data['email']['value'])) {
$email = new EmailEnrichFailed(
$data['email']['error'] === self::SERVER_ERROR ? $origin['email'] : $data['email']['value'],
$data['email']['error'] === self::VALIDATION_ERROR_TEXT, // checked must be true on validation error to prevent repeating requests
);
}
$domain = null;
if (isset($data['domain']['domain']) && array_key_exists('ip', $data['domain'])) {
try {
$domain = new DomainEnriched(
$data['domain']['domain'],
$data['domain']['blockdomains'],
$data['domain']['disposable_domains'],
$data['domain']['free_email_provider'],
$data['domain']['ip'],
$data['domain']['geo_ip'],
$data['domain']['geo_html'],
$data['domain']['web_server'],
$data['domain']['hostname'],
$data['domain']['emails'],
$data['domain']['phone'],
$data['domain']['discovery_date'],
$data['domain']['tranco_rank'],
$data['domain']['creation_date'],
$data['domain']['expiration_date'],
$data['domain']['return_code'],
$data['domain']['disabled'],
$data['domain']['closest_snapshot'],
$data['domain']['mx_record'],
);
} catch (\Throwable $e) {
$this->logger->logWarning('Error during parsing domain response', $e);
}
} elseif (isset($data['domain']['domain'])) {
$domain = new DomainNotFoundEnriched(
$data['domain']['domain'],
$data['domain']['blockdomains'],
$data['domain']['disposable_domains'],
$data['domain']['free_email_provider'],
$data['domain']['creation_date'],
$data['domain']['expiration_date'],
$data['domain']['return_code'],
$data['domain']['disabled'],
$data['domain']['closest_snapshot'],
$data['domain']['mx_record'],
);
} elseif (isset($data['domain']['value'])) {
$domain = new DomainEnrichFailed(
$data['domain']['error'] === self::SERVER_ERROR ? $origin['domain'] : $data['domain']['value'],
$data['domain']['error'] === self::VALIDATION_ERROR_TEXT, // checked must be true on validation error to prevent repeating requests
);
}
$ip = $isp = null;
if (isset($data['ip']['ip'])) {
try {
$ip = new IpAddressEnriched(
$data['ip']['ip'],
$data['ip']['country'],
$data['ip']['hosting'],
$data['ip']['vpn'],
$data['ip']['tor'],
$data['ip']['relay'],
$data['ip']['starlink'],
$data['ip']['blocklist'],
$data['ip']['domains_count'],
$data['ip']['cidr'],
$data['ip']['alert_list'],
);
if (isset($data['ip']['asn'])) {
$isp = new IspEnriched(
$data['ip']['asn'],
$data['ip']['name'],
$data['ip']['description'],
);
} else {
$isp = new IspEnrichedEmpty();
}
} catch (\Throwable $e) {
$this->logger->logWarning('Error during parsing IP response', $e);
}
} elseif (isset($data['ip']['error'])) {
if ($data['ip']['error'] === self::IP_IS_BOGON_ERROR_TEXT) {
$ip = new IpAddressLocalhostEnriched(
$data['ip']['value'],
);
$isp = new IspEnrichedLocalhost();
} else {
$ip = new IpAddressEnrichFailed(
$data['ip']['error'] === self::SERVER_ERROR ? $origin['ip'] : $data['ip']['value'],
$data['ip']['error'] === self::VALIDATION_ERROR_TEXT, // checked must be true on validation error to prevent repeating requests
);
$isp = new IspEnrichedEmpty();
}
}
$phone = null;
if (isset($data['phone']['phone_number']) && isset($data['phone']['profiles'])) {
try {
$phone = new PhoneEnriched(
$data['phone']['phone_number'],
$data['phone']['profiles'],
$data['phone']['iso_country_code'],
$data['phone']['calling_country_code'],
$data['phone']['national_format'],
$data['phone']['invalid'],
$data['phone']['validation_error'],
$data['phone']['carrier_name'],
$data['phone']['type'],
$data['phone']['alert_list'],
);
} catch (\Throwable $e) {
$this->logger->logWarning('Error during parsing phone response', $e);
}
} elseif (isset($data['phone']['validation_error'])) {
$this->logger->logWarning('Error getting phone from Enrichment API: ' . json_encode($data['phone']));
$phone = new PhoneInvalidEnriched(
$data['phone']['phone_number'],
$data['phone']['invalid'],
$data['phone']['validation_error'],
);
} elseif (isset($data['phone']['value'])) {
$phone = new PhoneEnrichFailed(
$data['phone']['error'] === self::SERVER_ERROR ? $origin['phone'] : $data['phone']['value'],
$data['phone']['error'] === self::VALIDATION_ERROR_TEXT, // checked must be true on validation error to prevent repeating requests
);
}
// Check/log errors
foreach ($data as $key => $value) {
if (isset($value['error'])) {
$this->logger->logWarning(sprintf(
'Error getting %s from Enrichment API: %s',
$key,
json_encode($value),
));
}
}
return new EnrichedData($email, $domain, $ip, $isp, $phone, true);
}
}

View File

@@ -0,0 +1,393 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Factory;
use Sensor\Entity\DeviceEntity;
use Sensor\Entity\DomainEnrichedEntity;
use Sensor\Entity\DomainEntity;
use Sensor\Entity\DomainNotFoundEntity;
use Sensor\Entity\EmailEnrichedEntity;
use Sensor\Entity\EmailEntity;
use Sensor\Entity\EventEntity;
use Sensor\Entity\IpAddressEnrichedEntity;
use Sensor\Entity\IpAddressLocalhostEnrichedEntity;
use Sensor\Entity\IpAddressEntity;
use Sensor\Entity\CountryEntity;
use Sensor\Entity\IspEntity;
use Sensor\Entity\IspEnrichedEntity;
use Sensor\Entity\IspLocalhostEntity;
use Sensor\Entity\PhoneEnrichedEntity;
use Sensor\Entity\PhoneEntity;
use Sensor\Entity\PhoneInvalidEntity;
use Sensor\Entity\RefererEntity;
use Sensor\Entity\SessionEntity;
use Sensor\Entity\UrlEntity;
use Sensor\Entity\UrlQueryEntity;
use Sensor\Entity\UserAgentEnrichedEntity;
use Sensor\Entity\UserAgentEntity;
use Sensor\Entity\PayloadEntity;
use Sensor\Model\Blacklist\FraudDetected;
use Sensor\Model\CreateEventDto;
use Sensor\Model\DeviceDetected;
use Sensor\Repository\CountryRepository;
use Sensor\Model\Enriched\EnrichedData;
use Sensor\Model\Enriched\IpAddressEnriched;
use Sensor\Model\Enriched\IpAddressLocalhostEnriched;
use Sensor\Model\Enriched\IpAddressEnrichFailed;
use Sensor\Model\Enriched\PhoneEnriched;
use Sensor\Model\Enriched\PhoneInvalidEnriched;
use Sensor\Model\Enriched\PhoneEnrichFailed;
use Sensor\Model\Enriched\DomainEnriched;
use Sensor\Model\Enriched\DomainNotFoundEnriched;
use Sensor\Model\Enriched\DomainEnrichFailed;
use Sensor\Model\Enriched\EmailEnriched;
use Sensor\Model\Enriched\EmailEnrichFailed;
class EventFactory {
public function __construct(
private CountryRepository $countryRepository,
) {
}
public function createFromDto(
int $accountId,
int $sessionId,
int $apiKeyId,
CreateEventDto $dto,
?EnrichedData $enrichedData,
FraudDetected $fraudDetected,
?DeviceDetected $deviceDetected,
?string $query,
): EventEntity {
$lastSeen = $dto->eventTime;
// Remove query from url
$urlWithoutQuery = $query !== null ? str_replace($query, '', $dto->url) : $dto->url;
$queryEntity = null;
if ($query !== null) {
$queryEntity = new UrlQueryEntity($apiKeyId, $query, $lastSeen);
}
$url = new UrlEntity($apiKeyId, $urlWithoutQuery, $queryEntity, $dto->pageTitle, $dto->httpCode, $lastSeen);
$eventType = $dto->eventType;
$httpMethod = $dto->httpMethod;
if ($dto->httpReferer !== null) {
$referer = new RefererEntity($apiKeyId, $dto->httpReferer, $lastSeen);
}
$session = new SessionEntity($sessionId, $accountId, $apiKeyId, $lastSeen);
// set countryId after inserting ip
$country = new CountryEntity($apiKeyId, 0, $lastSeen);
$ipAddress = null;
if ($dto->ipAddress->localhost) {
// sensor-defined localhost
$isp = new IspLocalhostEntity($apiKeyId, $lastSeen);
$ipAddress = new IpAddressLocalhostEnrichedEntity(
$apiKeyId,
$dto->ipAddress->value,
$dto->ipAddress->hash,
$fraudDetected->ip,
$isp,
$lastSeen,
);
} else {
if ($enrichedData?->ip instanceof IpAddressEnriched || $enrichedData?->ip instanceof IpAddressLocalhostEnriched) {
// isp IspEnriched or IspEnrichedEmpty
// it's a new ip, checked is true; should be INSERTed or UPDATEd
$countryId = $this->countryRepository->getCountryIdByCode($enrichedData->ip->countryCode);
$isp = new IspEnrichedEntity(
$apiKeyId,
$enrichedData->isp->asn,
$enrichedData->isp->name,
$enrichedData->isp->description,
$lastSeen,
);
$ipAddress = new IpAddressEnrichedEntity(
$apiKeyId,
$enrichedData->ip->ipAddress,
$dto->ipAddress->hash,
$countryId,
$enrichedData->ip->hosting,
$enrichedData->ip->vpn,
$enrichedData->ip->tor,
$enrichedData->ip->relay,
$enrichedData->ip->starlink,
$enrichedData->ip->blocklist,
$enrichedData->ip->domainsCount,
$enrichedData->ip->cidr,
$enrichedData->ip->alertList,
$fraudDetected->ip,
$isp,
true,
$lastSeen,
);
} elseif ($enrichedData?->ip instanceof IpAddressEnrichFailed) {
// checked can be false or true on bogon ip; should be INSERTed or UPDATEd (if it is reenrichment)
// isp made of IspEnrichedLocalhost or IspEnrichedEmpty
$isp = new IspEnrichedEntity(
$apiKeyId,
$enrichedData->isp->asn,
$enrichedData->isp->name,
$enrichedData->isp->description,
$lastSeen,
);
$ipAddress = new IpAddressEntity(
$apiKeyId,
$enrichedData->ip->ipAddress,
$dto->ipAddress->hash,
$fraudDetected->ip,
$isp,
$enrichedData->ip->checked,
$lastSeen,
);
} else {
// ip already exists and has checked true or enrichment is off; should be UPDATEd or INSERTed if enrichment is off
// isp unknown, N/A will be used if enrichment is off or failed
$isp = new IspEntity(
$apiKeyId,
$lastSeen,
);
$ipAddress = new IpAddressEntity(
$apiKeyId,
$dto->ipAddress->value,
$dto->ipAddress->hash,
$fraudDetected->ip,
$isp,
null, // enrichment is off or was enriched earlier
$lastSeen,
);
}
}
$domain = null;
if ($dto->emailDomain !== null) {
if ($enrichedData?->domain instanceof DomainEnriched) {
$domain = new DomainEnrichedEntity(
$apiKeyId,
$enrichedData->domain->domain,
$enrichedData->domain->blockdomains,
$enrichedData->domain->disposableDomains,
$enrichedData->domain->freeEmailProvider,
$enrichedData->domain->ip,
$enrichedData->domain->geoIp,
$enrichedData->domain->geoHtml,
$enrichedData->domain->webServer,
$enrichedData->domain->hostname,
$enrichedData->domain->emails,
$enrichedData->domain->phone,
$enrichedData->domain->discoveryDate,
$enrichedData->domain->trancoRank,
$enrichedData->domain->creationDate,
$enrichedData->domain->expirationDate,
$enrichedData->domain->returnCode,
$enrichedData->domain->disabled,
$enrichedData->domain->closestSnapshot,
$enrichedData->domain->mxRecord,
true,
$lastSeen,
);
} elseif ($enrichedData?->domain instanceof DomainNotFoundEnriched) {
$domain = new DomainNotFoundEntity(
$apiKeyId,
$enrichedData->domain->domain,
$enrichedData->domain->blockdomains,
$enrichedData->domain->disposableDomains,
$enrichedData->domain->freeEmailProvider,
$enrichedData->domain->creationDate,
$enrichedData->domain->expirationDate,
$enrichedData->domain->returnCode,
$enrichedData->domain->disabled,
$enrichedData->domain->closestSnapshot,
$enrichedData->domain->mxRecord,
true,
$lastSeen,
);
} elseif ($enrichedData?->domain instanceof DomainEnrichFailed) {
$domain = new DomainEntity(
$apiKeyId,
$enrichedData->domain->domain,
$enrichedData->domain->checked,
$lastSeen,
);
} else {
$domain = new DomainEntity(
$apiKeyId,
$dto->emailDomain,
null, // enrichment is off or was enriched earlier
$lastSeen,
);
}
}
$email = null;
if ($dto->emailAddress !== null && $domain !== null) {
if ($enrichedData?->email instanceof EmailEnriched) {
$email = new EmailEnrichedEntity(
$accountId,
$apiKeyId,
$enrichedData->email->email,
$dto->emailAddress->hash,
$domain,
$enrichedData->email->blockEmails,
$enrichedData->email->dataBreach,
$enrichedData->email->dataBreaches,
$enrichedData->email->earliestBreach,
$enrichedData->email->profiles,
$enrichedData->email->domainContactEmail,
$enrichedData->email->alertList,
$fraudDetected->email,
true,
$lastSeen,
);
} elseif ($enrichedData?->email instanceof EmailEnrichFailed) {
$email = new EmailEntity(
$accountId,
$apiKeyId,
$enrichedData->email->email,
$dto->emailAddress->hash,
$domain,
$fraudDetected->email,
$enrichedData->email->checked,
$lastSeen,
);
} else {
$email = new EmailEntity(
$accountId,
$apiKeyId,
$dto->emailAddress->value,
$dto->emailAddress->hash,
$domain,
$fraudDetected->email,
null, // enrichment is off or was enriched earlier
$lastSeen,
);
}
}
$phone = null;
if ($dto->phoneNumber !== null) {
$countryId = 0;
if ($enrichedData?->phone instanceof PhoneEnriched) {
if ($enrichedData->phone->countryCode !== null) {
$countryId = $this->countryRepository->getCountryIdByCode($enrichedData->phone->countryCode);
}
$phone = new PhoneEnrichedEntity(
$accountId,
$apiKeyId,
$enrichedData->phone->phoneNumber,
$dto->phoneNumber->hash,
$enrichedData->phone->profiles,
$countryId,
$enrichedData->phone->callingCountryCode,
$enrichedData->phone->nationalFormat,
$enrichedData->phone->invalid,
$enrichedData->phone->validationErrors,
$enrichedData->phone->carrierName,
$enrichedData->phone->type,
$enrichedData->phone->alertList,
$fraudDetected->phone,
true,
$lastSeen,
);
} elseif ($enrichedData?->phone instanceof PhoneInvalidEnriched) {
$phone = new PhoneInvalidEntity(
$accountId,
$apiKeyId,
$enrichedData->phone->phoneNumber,
$dto->phoneNumber->hash,
$countryId,
$fraudDetected->phone,
$enrichedData->phone->validationErrors,
true,
$lastSeen,
);
} elseif ($enrichedData?->phone instanceof PhoneEnrichFailed) {
$phone = new PhoneEntity(
$accountId,
$apiKeyId,
$enrichedData->phone->phoneNumber,
$dto->phoneNumber->hash,
$countryId,
$fraudDetected->phone,
$enrichedData->phone->checked,
$lastSeen,
);
} else {
$phone = new PhoneEntity(
$accountId,
$apiKeyId,
$dto->phoneNumber->value,
$dto->phoneNumber->hash,
$countryId, // set country to 0 if enrichment is off or keep existing country in query
$fraudDetected->phone,
null, // enrichment is off or was enriched earlier
$lastSeen,
);
}
}
$userAgent = null;
if ($deviceDetected instanceof DeviceDetected) {
$userAgent = new UserAgentEnrichedEntity(
$apiKeyId,
$deviceDetected->userAgent,
$deviceDetected->device,
$deviceDetected->browserName,
$deviceDetected->browserVersion,
$deviceDetected->osName,
$deviceDetected->osVersion,
$deviceDetected->modified,
true,
$lastSeen,
);
} else {
$userAgent = new UserAgentEntity(
$apiKeyId,
$dto->userAgent,
null, // ua is null, was enriched earlier or enrichment is off
$lastSeen,
);
}
$device = new DeviceEntity($accountId, $apiKeyId, $userAgent, $dto->browserLanguage, $lastSeen);
$payload = ($dto->payload) ? (new PayloadEntity($accountId, $apiKeyId, $dto->payload, $lastSeen)) : null;
return new EventEntity(
$accountId,
$session,
$apiKeyId,
$ipAddress,
$url,
$eventType,
$httpMethod,
$device,
$referer ?? null,
$email ?? null,
$phone ?? null,
$dto->httpCode,
$dto->eventTime,
$dto->traceId,
$payload,
$country,
);
}
}

View File

@@ -0,0 +1,86 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
namespace Sensor\Factory;
use Sensor\Entity\LogbookEntity;
use Sensor\Model\Http\ErrorResponse;
use Sensor\Model\Http\RegularResponse;
use Sensor\Model\Http\Request;
use Sensor\Model\Http\ValidationFailedResponse;
use Sensor\Repository\ApiKeyRepository;
class LogbookEntityFactory {
public function create(
int $apiKeyId,
\DateTime $startedTime,
RegularResponse|ErrorResponse $response,
): LogbookEntity {
$eventId = null;
if ($response instanceof RegularResponse) {
$errorText = $response->validationErrors();
$errorType = $errorText !== null
? LogbookEntity::ERROR_TYPE_VALIDATION_ERROR
: LogbookEntity::ERROR_TYPE_SUCCESS
;
$eventId = $response->eventId;
} elseif ($response instanceof ValidationFailedResponse) {
$errorType = LogbookEntity::ERROR_TYPE_CRITICAL_VALIDATION_ERROR;
$errorText = json_encode([$response->jsonSerialize()]);
} elseif ($response instanceof ErrorResponse) {
$errorType = LogbookEntity::ERROR_TYPE_CRITICAL_ERROR;
$errorText = json_encode([$response->jsonSerialize()]);
} else {
$errorType = LogbookEntity::ERROR_TYPE_CRITICAL_ERROR;
$errorText = json_encode(['Undefined error']);
}
return new LogbookEntity(
$apiKeyId,
$_SERVER['REMOTE_ADDR'],
$eventId,
$errorType,
$errorText,
$this->getRawRequest(),
$this->formatStarted($startedTime),
);
}
public function createFromException(
int $apiKeyId,
\DateTime $startedTime,
string $errorText,
): LogbookEntity {
return new LogbookEntity(
$apiKeyId,
$_SERVER['REMOTE_ADDR'],
null,
3,
$errorText,
$this->getRawRequest(),
$this->formatStarted($startedTime),
);
}
private function getRawRequest(): string {
return json_encode(array_intersect_key($_POST, array_flip(Request::ACCEPTABLE_FIELDS)));
}
private function formatStarted(\DateTime $startedTime): string {
$milliseconds = (int) ($startedTime->format('u') / 1000);
return $startedTime->format('Y-m-d H:i:s') . '.' . sprintf('%03d', $milliseconds);
}
}

View File

@@ -0,0 +1,158 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Factory;
use Sensor\Exception\ValidationException;
use Sensor\Model\CreateEventDto;
use Sensor\Model\HashedValue;
use Sensor\Model\Validated\Email;
use Sensor\Model\Validated\IpAddress;
use Sensor\Model\Validated\Phone;
use Sensor\Model\Validated\Timestamp;
use Sensor\Model\Validated\HttpCode;
use Sensor\Model\Validated\Firstname;
use Sensor\Model\Validated\Lastname;
use Sensor\Model\Validated\Fullname;
use Sensor\Model\Validated\HttpReferer;
use Sensor\Model\Validated\Userid;
use Sensor\Model\Validated\PageTitle;
use Sensor\Model\Validated\Url;
use Sensor\Model\Validated\UserAgent;
use Sensor\Model\Validated\BrowserLanguage;
use Sensor\Model\Validated\EventType;
use Sensor\Model\Validated\HttpMethod;
use Sensor\Model\Validated\UserCreated;
use Sensor\Model\Validated\Payloads\FieldEditPayload;
use Sensor\Model\Validated\Payloads\PageSearchPayload;
use Sensor\Model\Validated\Payloads\EmailChangePayload;
use Sensor\Repository\EventRepository;
use Sensor\Service\Constants;
class RequestFactory {
private const REQUIRED_FIELDS = ['ipAddress', 'url', 'eventTime'];
/**
* @param array<string, string> $data
*/
public static function createFromArray(array $data, ?string $traceId, EventRepository $eventRepository): CreateEventDto {
foreach (self::REQUIRED_FIELDS as $key) {
if (!isset($data[$key])) {
throw new ValidationException('Required field is missing or empty', $key);
}
}
$eventTime = new Timestamp($data['eventTime']);
$userCreated = isset($data['userCreated']) ? (new UserCreated($data['userCreated'])) : null;
$referer = isset($data['httpReferer']) ? (new HttpReferer($data['httpReferer'])) : null;
$httpCode = isset($data['httpCode']) ? (new HttpCode($data['httpCode'])) : null;
$ipAddress = new IpAddress($data['ipAddress']);
$phone = isset($data['phoneNumber']) ? (new Phone($data['phoneNumber'])) : null;
$email = isset($data['emailAddress']) ? (new Email($data['emailAddress'])) : null;
$firstname = isset($data['firstName']) ? (new Firstname($data['firstName'])) : null;
$lastname = isset($data['lastName']) ? (new Lastname($data['lastName'])) : null;
$fullname = isset($data['fullName']) ? (new Fullname($data['fullName'])) : null;
$username = isset($data['userName']) ? (new Userid($data['userName'])) : null;
$pageTitle = isset($data['pageTitle']) ? (new PageTitle($data['pageTitle'])) : null;
$url = isset($data['url']) ? (new Url($data['url'])) : null;
$userAgent = isset($data['userAgent']) ? (new UserAgent($data['userAgent'])) : null;
$browserLang = isset($data['browserLanguage']) ? (new BrowserLanguage($data['browserLanguage'])) : null;
$eventType = isset($data['eventType']) ? (new EventType($data['eventType'])) : null;
$httpMethod = isset($data['httpMethod']) ? (new HttpMethod($data['httpMethod'])) : null;
$payload = null;
$payloadRaw = $data['payload'] ?? null;
if ($eventType?->value) {
$eventTypeId = $eventRepository->getEventType($eventType->value);
if ($eventTypeId === Constants::FIELD_EDIT_EVENT_TYPE_ID) {
$payload = new FieldEditPayload($payloadRaw);
} elseif ($payloadRaw) {
switch ($eventTypeId) {
case Constants::ACCOUNT_EMAIL_CHANGE_EVENT_TYPE_ID:
$payload = new EmailChangePayload($payloadRaw);
break;
case Constants::PAGE_SEARCH_EVENT_TYPE_ID:
$payload = new PageSearchPayload($payloadRaw);
break;
}
}
}
$validatedParams = [
$email, $eventTime, $userCreated, $referer, $httpCode, $ipAddress,
$phone, $firstname, $lastname, $fullname, $username, $pageTitle,
$url, $userAgent, $browserLang, $eventType, $httpMethod, $payload,
];
$changedParams = self::changedParams($validatedParams);
//$email = !isset($data['emailAddress']) && !isset($data['userName']) ? Email::makePlaceholder() : $email;
//$username = $username === null ? md5($email->value) : $username->value;
if ($email === null && $username === null) {
$username = 'N/A';
} else {
$username = $username === null ? md5($email->value) : $username->value;
}
return new CreateEventDto(
$firstname?->value,
$lastname?->value,
$fullname?->value,
$pageTitle?->value,
$username,
$email !== null ? new HashedValue($email) : null,
$email !== null ? explode('@', $email->value)[1] : null,
$phone !== null && !$phone->isEmpty() ? new HashedValue($phone) : null,
new HashedValue($ipAddress),
$url?->value,
$userAgent?->value,
$eventTime->value,
$referer?->value,
$httpCode?->value,
$browserLang?->value,
$eventType?->value,
$httpMethod?->value,
$userCreated?->value,
$traceId,
$payload?->value,
$changedParams,
);
}
private static function changedParams(array $validatedParams): array {
$result = [];
foreach ($validatedParams as $param) {
if ($param !== null) {
$validation = $param->validationStatement();
if ($validation !== null) {
$result[] = $validation;
}
}
}
return $result;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Blacklist;
class FraudDetected {
public function __construct(
public bool $email,
public bool $ip,
public bool $phone,
) {
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Config;
class Config implements \JsonSerializable {
public function __construct(
public DatabaseConfig $databaseConfig,
public ?string $enrichmentApiUrl = null,
#[\SensitiveParameter]
public ?string $enrichmentApiKey = null,
public ?string $scoreApiUrl = null,
public ?string $userAgent = null,
public bool $debugLog = false,
public bool $allowEmailPhone = false,
) {
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array {
return [
'databaseConfig' => $this->databaseConfig,
'enrichmentApiUrl' => $this->enrichmentApiUrl,
'scoreApiUrl' => $this->scoreApiUrl,
'debugLog' => $this->debugLog,
];
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Config;
class DatabaseConfig implements \JsonSerializable {
public function __construct(
public string $dbHost,
public int $dbPort,
public string $dbUser,
#[\SensitiveParameter]
public string $dbPassword,
public string $dbDatabaseName,
) {
}
/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array {
return [
'dbHost' => $this->dbHost,
'dbPort' => $this->dbPort,
'dbUser' => $this->dbUser,
'dbDatabaseName' => $this->dbDatabaseName,
];
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model;
class CreateEventDto {
/**
* @param array<mixed, mixed>|null $payload
*/
public function __construct(
public ?string $firstName,
public ?string $lastName,
public ?string $fullName,
public ?string $pageTitle,
public string $userName,
public ?HashedValue $emailAddress,
public ?string $emailDomain,
public ?HashedValue $phoneNumber,
public HashedValue $ipAddress,
public string $url,
public ?string $userAgent,
public \DateTimeImmutable $eventTime,
public ?string $httpReferer,
public ?int $httpCode,
public ?string $browserLanguage,
public ?string $eventType,
public ?string $httpMethod,
public ?\DateTimeImmutable $userCreated,
public ?string $traceId,
public array|string|null $payload,
public array $changedParams,
) {
}
}

View File

@@ -0,0 +1,31 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model;
class DeviceDetected {
public function __construct(
public ?string $device,
public ?string $browserName,
public ?string $browserVersion,
public ?string $osName,
public ?string $osVersion,
public string $userAgent,
public bool $modified,
) {
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class DomainEnrichFailed {
public function __construct(
public string $domain,
public bool $checked = false, // must be true on validation error to prevent repeating requests
) {
}
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class DomainEnriched {
public function __construct(
public string $domain,
public bool $blockdomains,
public bool $disposableDomains,
public bool $freeEmailProvider,
public ?string $ip,
public ?string $geoIp,
public ?string $geoHtml,
public ?string $webServer,
public ?string $hostname,
public ?string $emails,
public ?string $phone,
public string $discoveryDate,
public ?int $trancoRank,
public ?string $creationDate,
public ?string $expirationDate,
public ?int $returnCode,
public bool $disabled,
public ?string $closestSnapshot,
public bool $mxRecord,
) {
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class DomainNotFoundEnriched {
public function __construct(
public string $domain,
public bool $blockdomains,
public bool $disposableDomains,
public bool $freeEmailProvider,
public ?string $creationDate,
public ?string $expirationDate,
public ?int $returnCode,
public bool $disabled,
public ?string $closestSnapshot,
public bool $mxRecord,
) {
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class EmailEnrichFailed {
public function __construct(
public string $email,
public bool $checked = false, // must be true on validation error to prevent repeating requests
) {
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class EmailEnriched {
public function __construct(
public string $email,
public bool $blockEmails,
public bool $dataBreach,
public int $dataBreaches,
public ?\DateTimeImmutable $earliestBreach,
public int $profiles,
public bool $domainContactEmail,
public ?bool $alertList,
) {
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class EnrichedData {
public function __construct(
public EmailEnriched|EmailEnrichFailed|null $email,
public DomainEnriched|DomainNotFoundEnriched|DomainEnrichFailed|null $domain,
public IpAddressEnriched|IpAddressLocalhostEnriched|IpAddressEnrichFailed|null $ip,
public IspEnriched|IspEnrichedEmpty|IspEnrichedLocalhost|null $isp, // TODO: move to IP
public PhoneEnriched|PhoneInvalidEnriched|PhoneEnrichFailed|null $phone,
public bool $reached,
) {
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class IpAddressEnrichFailed {
public function __construct(
public string $ipAddress,
public bool $checked = false, // must be true on validation error to prevent repeating requests
) {
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class IpAddressEnriched {
/**
* @param string[] $domainsCount
*/
public function __construct(
public string $ipAddress,
public string $countryCode,
public bool $hosting,
public bool $vpn,
public bool $tor,
public bool $relay,
public bool $starlink,
public bool $blocklist,
public array $domainsCount,
public string $cidr,
public ?bool $alertList,
) {
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class IpAddressLocalhostEnriched {
/**
* @param string[] $domainsCount
*/
public function __construct(
public string $ipAddress,
public string $countryCode = 'N/A',
public bool $hosting = false,
public bool $vpn = false,
public bool $tor = false,
public bool $relay = false,
public bool $starlink = false,
public bool $blocklist = false,
public ?array $domainsCount = [],
public ?string $cidr = null,
public ?bool $alertList = null,
) {
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class IspEnriched {
public function __construct(
public int $asn,
public ?string $name,
public ?string $description,
) {
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class IspEnrichedEmpty {
public function __construct(
public int $asn = 64496,
public ?string $name = 'N/A',
public ?string $description = null,
) {
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class IspEnrichedLocalhost {
public function __construct(
public int $asn = 0,
public ?string $name = 'Local area network',
public ?string $description = null,
) {
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class PhoneEnrichFailed {
public function __construct(
public string $phoneNumber,
public bool $checked = false, // must be true on validation error to prevent repeating requests
) {
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class PhoneEnriched {
public function __construct(
public string $phoneNumber,
public int $profiles,
public ?string $countryCode,
public int $callingCountryCode,
public string $nationalFormat,
public bool $invalid,
public ?string $validationErrors,
public ?string $carrierName,
public string $type,
public ?bool $alertList,
) {
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Enriched;
class PhoneInvalidEnriched {
public function __construct(
public string $phoneNumber,
public bool $invalid,
public string $validationErrors,
) {
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model;
use Sensor\Model\Validated\Email;
use Sensor\Model\Validated\IpAddress;
use Sensor\Model\Validated\Phone;
class HashedValue {
public string $value;
public string $hash;
public ?bool $localhost = null;
public function __construct(
Email|IpAddress|Phone $input,
) {
$this->value = $input->value;
$this->hash = hash('sha256', $this->value);
if ($input instanceof IpAddress) {
$this->localhost = $input->isLocalhost();
}
}
/**
* @return array{value: string, hash: ?string}
*/
public function toArray(bool $hashExchange): array {
return [
'value' => $this->value,
'hash' => $hashExchange ? $this->hash : null,
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Http;
class ErrorResponse {
public function __construct(
public string $errorMessage,
public int $httpCode,
) {
}
public function __toString(): string {
return $this->errorMessage;
}
public function jsonSerialize() {
return $this->__toString();
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Http;
class RegularResponse {
public function __construct(
public int $eventId,
public array $changedParams,
) {
}
public function validationErrors(): ?string {
return count($this->changedParams) ? json_encode($this->changedParams) : null;
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Http;
class Request {
public const ACCEPTABLE_FIELDS = [
'userName',
'emailAddress',
'ipAddress',
'url',
'userAgent',
'eventTime',
'firstName',
'lastName',
'fullName',
'pageTitle',
'phoneNumber',
'httpReferer',
'httpCode',
'browserLanguage',
'eventType',
'httpMethod',
'userCreated',
'payload',
];
/**
* @param array<string,string> $body
*/
public function __construct(
public array $body,
#[\SensitiveParameter]
public ?string $apiKey,
public ?string $traceId,
) {
// all acceptable $this->body values should be either string or null
foreach (self::ACCEPTABLE_FIELDS as $key) {
if (isset($this->body[$key])) {
$value = $this->body[$key];
if (is_bool($value)) {
$this->body[$key] = ($value) ? 'true' : 'false';
} elseif (is_array($value)) {
$this->body[$key] = $this->cleanArrayEncoding($value);
if ($key !== 'payload') {
$this->body[$key] = json_encode($this->body[$key]);
}
} elseif ($value !== null) {
$this->body[$key] = $this->cleanArrayEncoding(strval($value));
}
} else {
$this->body[$key] = null;
}
}
$this->body['hashEmailAddress'] = null;
$this->body['hashPhoneNumber'] = null;
$this->body['hashIpAddress'] = null;
}
// recursive array encoding cleanup
private function cleanArrayEncoding(mixed $data): mixed {
if (is_string($data)) {
return mb_convert_encoding($data, 'UTF-8', 'UTF-8');
} elseif (is_array($data)) {
foreach ($data as $key => $value) {
$data[$key] = $this->cleanArrayEncoding($value);
}
}
return $data;
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Http;
use Sensor\Exception\ValidationException;
class ValidationFailedResponse extends ErrorResponse {
public function __construct(
private ValidationException $exception,
) {
parent::__construct('Validation error', 400);
}
public function __toString(): string {
return sprintf(
'Validation error: "%s" for key "%s"',
$this->exception->getMessage(),
$this->exception->getKey(),
);
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Score;
class ScoreAccount {
public function __construct(
public string $apiKey,
public int $accountId,
public string $userId,
) {
}
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class Base {
private string $type;
public bool $invalid;
public string $origin;
public function __construct(string $value, string $type) {
$this->origin = $value;
$this->type = $type;
}
public function validationStatement(): ?string {
if ($this->invalid) {
return "$this->type validation error on `$this->origin`";
}
return null;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class BrowserLanguage extends Length {
public function __construct(string $value) {
parent::__construct($value, 'browserLanguage', 255);
}
}

View File

@@ -0,0 +1,58 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class Email extends Length {
private const PLACEHOLDER = 'unknown@localhost';
private const INVALIDPLACEHOLDER = 'broken@user.local';
public string $value;
public function __construct(string $value) {
parent::__construct($value, 'emailAddress', 255);
$value = strtolower(str_replace(' ', '', $value));
if (!self::isPlaceholder($value) && filter_var($value, \FILTER_VALIDATE_EMAIL) === false) {
$this->value = self::INVALIDPLACEHOLDER;
} else {
$this->value = $value;
}
$this->invalid = $this->invalid || (!self::isPlaceholder($value) && filter_var($value, \FILTER_VALIDATE_EMAIL) === false);
}
public static function makePlaceholder(): self {
return new self(self::PLACEHOLDER);
}
public static function isPlaceholder(string $email): bool {
return self::PLACEHOLDER === $email;
}
public static function isInvalid(string $value): bool {
return self::INVALIDPLACEHOLDER === $value;
}
public static function isPlaceholderDomain(string $email): bool {
return explode('@', self::PLACEHOLDER)[1] === $email;
}
public static function isInvalidDomain(string $value): bool {
return explode('@', self::INVALIDPLACEHOLDER)[1] === $value;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class EventType extends Length {
public function __construct(string $value) {
parent::__construct($value, 'eventType');
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class Firstname extends Length {
public function __construct(string $value) {
parent::__construct($value, 'firstName');
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class Fullname extends Length {
public function __construct(string $value) {
parent::__construct($value, 'fullName');
}
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class HttpCode extends Base {
private const INVALIDPLACEHOLDER = '0';
public int $value;
public function __construct(string $value) {
parent::__construct($value, 'httpCode');
$this->value = (int) (ctype_digit($value) ? $value : self::INVALIDPLACEHOLDER);
$this->invalid = !ctype_digit($value);
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class HttpMethod extends Length {
public function __construct(string $value) {
parent::__construct($value, 'httpMethod');
}
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class HttpReferer extends Length {
public function __construct(string $value) {
parent::__construct($value, 'httpReferer', 2047);
$this->value = urldecode($this->value);
}
}

View File

@@ -0,0 +1,80 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class IpAddress extends Base {
private const INVALIDPLACEHOLDER = '0.0.0.0';
private const LOCALHOST_NETS = [
'0.0.0.0/8', '10.0.0.0/8', '100.64.0.0/10', '127.0.0.0/8',
'169.254.0.0/16', '172.16.0.0/12', '192.0.0.0/24', '192.0.2.0/24',
'192.168.0.0/16', '198.18.0.0/15', '198.51.100.0/24', '203.0.113.0/24',
'224.0.0.0/4', '240.0.0.0/4', '255.255.255.255/32',
'::/128', '::1/128', '::ffff:0:0/96', '::/96', '100::/64', '2001:10::/28',
'2001:db8::/32', 'fc00::/7', 'fe80::/10', 'fec0::/10', 'ff00::/8',
];
public string $value;
public function __construct(string $value) {
parent::__construct($value, 'ipAddress');
$value = str_replace(' ', '', $value);
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) === false) {
$this->value = self::INVALIDPLACEHOLDER;
} else {
$this->value = $value;
}
$this->invalid = filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) === false;
}
public static function isInvalid(string $value): bool {
return self::INVALIDPLACEHOLDER === $value;
}
public function isLocalhost(): bool {
if (filter_var($this->value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) === false) {
return false;
}
$size = filter_var($this->value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? 4 : 16;
$ip = inet_pton($this->value);
foreach (self::LOCALHOST_NETS as $cidr) {
[$net, $maskBits] = explode('/', $cidr);
$net = inet_pton($net);
if (!$net) {
continue;
}
$solid = (int) floor($maskBits / 8);
$solidBits = $solid * 8;
$mask = str_repeat(chr(255), $solid);
for ($i = $solidBits; $i < $maskBits; $i += 8) {
$bits = max(0, min(8, $maskBits - $i));
$mask .= chr((pow(2, $bits) - 1) << (8 - $bits));
}
$mask = str_pad($mask, $size, chr(0));
if (($ip & $mask) === ($net & $mask)) {
return true;
};
}
return false;
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class Lastname extends Length {
public function __construct(string $value) {
parent::__construct($value, 'lastName');
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class Length extends Base {
public string $value;
public function __construct(string $value, string $type, int $limit = 100) {
parent::__construct($value, $type);
if (strlen($value) > $limit) {
$this->value = substr($value, 0, $limit);
} else {
// even if empty string!
$this->value = $value;
}
$this->invalid = strlen($value) > $limit;
}
public function isEmpty(): bool {
return $this->value === '';
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class PageTitle extends Length {
public function __construct(string $value) {
parent::__construct($value, 'pageTitle', 255);
}
}

View File

@@ -0,0 +1,98 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated\Payloads;
class Base extends \Sensor\Model\Validated\Base {
protected array $optionalFields;
protected array $requiredFields;
protected bool $set;
protected bool $dump;
public array|string|null $value;
public function __construct(mixed $value) {
parent::__construct(json_encode($value), 'payload');
if (!is_array($value)) {
$this->invalid = true;
$this->value = null;
return;
}
$this->invalid = false;
$data = [];
if (!$this->set) {
$data = $this->preserveItem($value);
} else {
foreach ($value as $payload) {
$item = [];
if ($payload && is_array($payload)) {
$item = $this->preserveItem($payload);
if ($item && count($item)) {
$data[] = $item;
} else {
$this->invalid = true;
}
} else {
$this->invalid = true;
}
}
}
if (!count($data)) {
$this->invalid = true;
}
$this->value = count($data) ? ($this->dump ? json_encode($data) : $data) : null;
}
private function preserveItem(array $item): ?array {
$data = [];
foreach ($this->requiredFields as $key) {
if (isset($item[$key])) {
$data[$key] = $this->convert($item[$key]);
} else {
$data[$key] = null;
$this->invalid = true;
}
}
foreach ($this->optionalFields as $key) {
$data[$key] = isset($item[$key]) ? $this->convert($item[$key]) : null;
}
return count($data) ? $data : null;
}
private function convert(mixed $val): ?string {
if (is_array($val)) {
return json_encode($val);
}
if (is_int($val) || is_float($val) || is_string($val) || is_bool($val)) {
return strval($val);
}
return null;
}
}

View File

@@ -0,0 +1,35 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated\Payloads;
class EmailChangePayload extends Base {
public function __construct(mixed $value) {
$this->requiredFields = [
'new_value',
];
$this->optionalFields = [
'old_value',
];
$this->set = false;
$this->dump = true;
parent::__construct($value);
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated\Payloads;
class FieldEditPayload extends Base {
public function __construct(mixed $value) {
$this->requiredFields = [
'new_value',
];
$this->optionalFields = [
'field_id',
'field_name',
'old_value',
'parent_id',
'parent_name',
];
$this->set = true;
$this->dump = false;
parent::__construct($value);
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated\Payloads;
class PageSearchPayload extends Base {
public function __construct(mixed $value) {
$this->requiredFields = [
'field_id',
'value',
];
$this->optionalFields = [
'field_name',
];
$this->set = false;
$this->dump = true;
parent::__construct($value);
}
}

View File

@@ -0,0 +1,41 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class Phone extends Base {
private const MAX_PHONE_LENGTH = 19;
public string $value;
public function __construct(string $value) {
parent::__construct($value, 'phoneNumber');
$value = str_replace(' ', '', $value);
if (strlen($value) > self::MAX_PHONE_LENGTH) {
$this->value = substr($value, 0, self::MAX_PHONE_LENGTH);
} else {
// even if empty string!
$this->value = $value;
}
$this->invalid = strlen($value) > self::MAX_PHONE_LENGTH;
}
public function isEmpty(): bool {
return $this->value === '';
}
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class Timestamp extends Base {
public const EVENTFORMAT = 'Y-m-d H:i:s.v';
public const FORMAT = 'Y-m-d H:i:s';
public const MICROSECONDS = 'Y-m-d H:i:s.u';
public \DateTimeImmutable $value;
public function __construct(string $value) {
parent::__construct($value, 'timestamp');
$invalid = false;
try {
$val = \DateTimeImmutable::createFromFormat(self::EVENTFORMAT, $value);
} catch (\Throwable $e) {
// \DateTimeImmutable::createFromFormat throws ValueError when the datetime contains NULL-bytes
$invalid = true;
$val = self::currentTime();
}
if ($val === false) {
$val = \DateTimeImmutable::createFromFormat(self::FORMAT, $value);
}
if ($val === false) {
$val = \DateTimeImmutable::createFromFormat(self::MICROSECONDS, $value);
}
if ($val === false) {
$invalid = true;
$val = self::currentTime();
}
$this->value = $val;
$this->invalid = $invalid;
}
private function currentTime(): \DateTimeImmutable {
return new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* Tirreno ~ Open source user analytics
* Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Tirreno Technologies Sàrl (https://www.tirreno.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.tirreno.com Tirreno(tm)
*/
declare(strict_types=1);
namespace Sensor\Model\Validated;
class Url extends Length {
public function __construct(string $value) {
parent::__construct($value, 'url', 2047);
}
}

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