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,70 @@
<?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 Models\Api;
class Users extends \Models\BaseSql {
protected $DB_TABLE_NAME = 'event_account';
public function getUsersByApiKey(?int $userId, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':user_url' => \Utils\Variables::getSiteWithProtocol() . '/id/',
];
$query = (
'SELECT
-- create user url
:user_url || event_account.id AS internal_url,
-- -event_account.id,
event_account.userid,
event_account.created,
-- event_account.key,
event_account.lastip,
event_account.lastemail,
event_account.lastphone,
event_account.lastseen,
event_account.fullname,
event_account.firstname,
event_account.lastname,
-- event_account.is_important,
event_account.total_visit,
event_account.total_country,
event_account.total_ip,
event_account.total_device,
event_account.total_shared_ip,
event_account.total_shared_phone,
event_account.score_updated_at,
event_account.score,
event_account.score_details,
event_account.reviewed,
event_account.fraud,
event_account.latest_decision
FROM event_account'
);
$where = ' WHERE key = :api_key';
if ($userId !== null) {
$params[':user_id'] = $userId;
$where .= ' AND event_account.id = :user_id';
}
$query .= $where;
return $this->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,67 @@
<?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 Models;
class ApiKeyCoOwner extends \Models\BaseSql {
protected $DB_TABLE_NAME = 'dshb_api_co_owners';
public function getCoOwnership(int $operatorId): self|null|false {
$filters = [
'operator=?', $operatorId,
];
return $this->load($filters);
}
public function getSharedApiKeyOperators(int $operatorId): array {
$params = [
':creator' => $operatorId,
];
$query = (
'SELECT
dshb_operators.id,
dshb_operators.email,
dshb_operators.is_active
FROM
dshb_api
JOIN dshb_api_co_owners
ON dshb_api.id = dshb_api_co_owners.api
JOIN dshb_operators
ON dshb_api_co_owners.operator = dshb_operators.id
WHERE
dshb_api.creator = :creator;'
);
return $this->execQuery($query, $params);
}
public function create(int $operator, int $api): void {
$this->operator = $operator;
$this->api = $api;
$this->save();
}
public function deleteCoOwnership(): void {
if ($this->loaded()) {
$this->erase();
}
}
}

View File

@@ -0,0 +1,207 @@
<?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 Models;
class ApiKeys extends \Models\BaseSql {
protected $DB_TABLE_NAME = 'dshb_api';
public function add(array $data): int {
$quote = $data['quote'];
$operatorId = $data['operator_id'];
$uuid = sprintf('%s%s%s', $operatorId, $quote, time());
$this->quote = $quote;
$this->creator = $operatorId;
$this->key = $this->getHash($uuid);
if (array_key_exists('skip_enriching_attributes', $data)) {
$this->skip_enriching_attributes = $data['skip_enriching_attributes'];
}
if (array_key_exists('skip_blacklist_sync', $data)) {
$this->skip_blacklist_sync = $data['skip_blacklist_sync'];
}
$this->save();
return (int) $this->id;
}
public function getKeys(int $operatorId): array {
$filters = [
'creator=?', $operatorId,
];
return $this->find($filters);
}
public function getKey(int $operatorId): ?ApiKeys {
$keys = $this->getKeys($operatorId);
return $keys[0] ?? null;
}
public function resetKey(int $keyId, int $operatorId): void {
$this->getByKeyAndOperatorId($keyId, $operatorId);
if ($this->loaded()) {
$uuid = sprintf('%s%s%s', $keyId, $operatorId, time());
$this->key = $this->getHash($uuid);
$this->save();
}
}
public function getByKeyAndOperatorId(int $keyId, int $operatorId): self|null|false {
$filters = [
'id=? AND creator=?', $keyId, $operatorId,
];
return $this->load($filters);
}
public function getKeyIdByHash(string $hash): self|null|false {
$filters = [
'key=?', $hash,
];
return $this->load($filters);
}
public function getKeyById(int $keyId): self|null|false {
$filters = [
'id=?', $keyId,
];
return $this->load($filters);
}
public function getTimezoneByKeyId(int $keyId): string {
$params = [
':api_key' => $keyId,
];
$query = (
'SELECT
dshb_operators.timezone
FROM
dshb_api
JOIN dshb_operators
ON dshb_operators.id = dshb_api.creator
WHERE
dshb_api.id = :api_key'
);
$results = $this->execQuery($query, $params);
return $results[0]['timezone'] ?? 'UTC';
}
public function getSkipEnrichingAttributes(int $keyId): array {
$params = [
':api_key' => $keyId,
];
$query = (
'SELECT
dshb_api.skip_enriching_attributes
FROM dshb_api
WHERE
dshb_api.id = :api_key'
);
$results = $this->execQuery($query, $params);
if (!count($results)) {
return [];
}
$results = json_decode($results[0]['skip_enriching_attributes']);
if (!\Utils\Variables::getEmailPhoneAllowed()) {
if (!in_array('email', $results, true)) {
$results[] = 'email';
}
if (!in_array('phone', $results, true)) {
$results[] = 'phone';
}
if (!in_array('domain', $results, true)) {
$results[] = 'domain';
}
}
return $results;
}
public function enrichableAttributes(int $keyId): array {
$skipAttributes = $this->getSkipEnrichingAttributes($keyId);
$attributes = \Utils\Constants::get('ENRICHING_ATTRIBUTES');
$attributes = array_diff_key($attributes, array_flip($skipAttributes));
return $attributes;
}
public function attributeIsEnrichable(string $attr, int $keyId): bool {
return array_key_exists($attr, $this->enrichableAttributes($keyId));
}
public function getAllApiKeyIds(): array {
$query = 'SELECT id from dshb_api';
return $this->execQuery($query, null);
}
public function updateSkipEnrichingAttributes(array $attributes): void {
if ($this->loaded()) {
$attributes = \array_values($attributes);
$this->skip_enriching_attributes = \json_encode($attributes);
$this->save();
}
}
public function updateSkipBlacklistSynchronisation(bool $skip): void {
if ($this->loaded()) {
$this->skip_blacklist_sync = $skip;
$this->save();
}
}
public function updateRetentionPolicy(int $policyInWeeks): void {
if ($this->loaded()) {
$this->retention_policy = $policyInWeeks;
$this->save();
}
}
public function updateBlacklistThreshold(int $value): void {
if ($this->loaded()) {
$this->blacklist_threshold = $value;
$this->save();
}
}
public function updateReviewQueueThreshold(int $value): void {
if ($this->loaded()) {
$this->review_queue_threshold = $value;
$this->save();
}
}
public function updateInternalToken(string $apiToken): void {
if ($this->loaded()) {
$this->token = $apiToken;
$this->save();
}
}
}

View File

@@ -0,0 +1,76 @@
<?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 Models;
abstract class BaseSql extends \DB\SQL\Mapper {
use \Traits\Debug;
protected $f3 = null;
protected $DB_TABLE_TTL = 0;
protected $DB_TABLE_NAME = null;
protected $DB_TABLE_FIELDS = null;
public function __construct() {
$this->f3 = \Base::instance();
if ($this->DB_TABLE_NAME) {
$DB = $this->getDatabaseConnection();
parent::__construct($DB, $this->DB_TABLE_NAME, $this->DB_TABLE_FIELDS, $this->DB_TABLE_TTL);
}
}
private function getDatabaseConnection(): ?\DB\SQL {
return $this->f3->get('API_DATABASE');
}
public function getHash(string $string): string {
$iterations = 1000;
$salt = $this->f3->get('SALT');
return hash_pbkdf2('sha256', $string, $salt, $iterations, 32);
}
public function getPseudoRandomString(int $length = 32): string {
$bytes = \openssl_random_pseudo_bytes($length / 2);
return \bin2hex($bytes);
}
public function printLog(): void {
echo $this->f3->get('API_DATABASE')->log();
}
public function getArrayPlaceholders(array $ids, string $postfix = ''): array {
$params = [];
$placeHolders = [];
$postfix = $postfix !== '' ? '_' . $postfix : '';
foreach ($ids as $i => $id) {
$key = sprintf(':item_id_%s%s', $i, $postfix);
$placeHolders[] = $key;
$params[$key] = $id;
}
$placeHolders = implode(', ', $placeHolders);
return [$params, $placeHolders];
}
public function execQuery(string $query, ?array $params): array|int|null {
return $this->getDatabaseConnection()->exec($query, $params);
}
}

View File

@@ -0,0 +1,109 @@
<?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 Models;
class BlacklistItems extends \Models\BaseSql {
protected $DB_TABLE_NAME = 'event';
public function getIpsRelatedToAccountWithinOperator(int $accountId, int $apiKey): array {
$params = [
':account_id' => $accountId,
':api_key' => $apiKey,
];
$query = ("
SELECT
event_ip.id,
event_ip.ip AS value,
'ip' AS type,
:account_id::bigint AS account_id
FROM event_ip
WHERE
(id, key) IN (
SELECT ip, key
FROM event
WHERE
account = :account_id
AND key = :api_key)");
return $this->execQuery($query, $params);
}
public function getEmailsRelatedToAccountWithinOperator(int $accountId, int $apiKey): array {
$params = [
':account_id' => $accountId,
':api_key' => $apiKey,
];
$query = ("
SELECT
id,
email AS value,
'email' AS type,
account_id
FROM
event_email
WHERE
key = :api_key
AND account_id = :account_id
");
return $this->execQuery($query, $params);
}
public function getPhonesRelatedToAccountWithinOperator(int $accountId, int $apiKey): array {
$params = [
':account_id' => $accountId,
':api_key' => $apiKey,
];
$query = ("
SELECT
id,
phone_number AS value,
'phone' AS type,
account_id
FROM
event_phone
WHERE
key = :api_key
AND account_id = :account_id
");
return $this->execQuery($query, $params);
}
public function searchBlacklistedItem(string $value, int $apiKey): ?bool {
$query = '';
$params = [
':value' => $value,
':api_key' => $apiKey,
];
$query = ('
SELECT 1
FROM event_account
WHERE
userid = :value AND
fraud IS TRUE AND
key = :api_key
LIMIT 1');
$results = $this->execQuery($query, $params);
return (bool) count($results);
}
}

View File

@@ -0,0 +1,99 @@
<?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 Models;
class Bot extends \Models\BaseSql implements \Interfaces\ApiKeyAccessAuthorizationInterface {
protected $DB_TABLE_NAME = 'event_ua_parsed';
public function checkAccess(int $subjectId, int $apiKey): bool {
$query = (
'SELECT
event_ua_parsed.id
FROM
event_ua_parsed
WHERE
event_ua_parsed.key = :api_key
AND event_ua_parsed.id = :ua_id'
);
$params = [
':api_key' => $apiKey,
':ua_id' => $subjectId,
];
$results = $this->execQuery($query, $params);
return count($results) > 0;
}
public function getFullBotInfoById(int $uaId, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':ua_id' => $uaId,
];
$query = (
'SELECT
event_ua_parsed.id,
event_ua_parsed.device,
event_ua_parsed.device AS title,
event_ua_parsed.browser_name,
event_ua_parsed.browser_version,
event_ua_parsed.os_name,
event_ua_parsed.os_version,
event_ua_parsed.ua,
event_ua_parsed.modified,
event_ua_parsed.checked
FROM
event_ua_parsed
WHERE
event_ua_parsed.key = :api_key AND
event_ua_parsed.id = :ua_id'
);
$results = $this->execQuery($query, $params);
return $results[0] ?? [];
}
public function extractById(int $entityId, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':id' => $entityId,
];
$query = (
"SELECT
COALESCE(event_ua_parsed.ua, '') AS value
FROM
event_ua_parsed
WHERE
event_ua_parsed.key = :api_key
AND event_ua_parsed.id = :id
LIMIT 1"
);
$results = $this->execQuery($query, $params);
return $results[0] ?? [];
}
}

View File

@@ -0,0 +1,57 @@
<?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 Models;
class ChangeEmail extends \Models\BaseSql {
protected $DB_TABLE_NAME = 'dshb_operators_change_email';
public function add(int $operatorId, string $email): void {
$record = $this->getUnusedKeyByOperatorId($operatorId);
if ($record) {
$this->status = 'invalidated';
$this->save();
}
$this->reset();
$this->renew_key = $this->getPseudoRandomString(32);
$this->operator_id = $operatorId;
$this->email = $email;
$this->status = 'unused';
$this->save();
}
public function getUnusedKeyByOperatorId(int $operatorId): self|null|false {
return $this->load(
['"operator_id"=? AND "status"=?', $operatorId, 'unused'],
);
}
public function getByRenewKey(string $key): self|null|false {
return $this->load(
['"renew_key"=? AND "status"=?', $key, 'unused'],
);
}
public function deactivate(): void {
if ($this->loaded()) {
$this->status = 'used';
$this->save();
}
}
}

View File

@@ -0,0 +1,163 @@
<?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 Models\Chart;
abstract class Base extends \Models\BaseSql {
use \Traits\DateRange;
protected function concatDataLines(array $data1, string $field1, array $data2, string $field2, array $data3 = [], ?string $field3 = null): array {
$data0 = [];
$iters = count($data1);
for ($i = 0; $i < $iters; ++$i) {
$item = $data1[$i];
$ts = $item['ts'];
$data0[$ts] = [
'ts' => $ts,
$field1 => $item[$field1],
$field2 => 0,
];
if ($field3) {
$data0[$ts][$field3] = 0;
}
}
$iters = count($data2);
for ($i = 0; $i < $iters; ++$i) {
$item = $data2[$i];
$ts = $item['ts'];
if (!array_key_exists($ts, $data0)) {
$data0[$ts] = [
'ts' => $ts,
$field1 => 0,
$field2 => 0,
];
if ($field3) {
$data0[$ts][$field3] = 0;
}
}
$data0[$ts][$field2] = $item[$field2];
}
$iters = count($data3);
for ($i = 0; $i < $iters; ++$i) {
$item = $data3[$i];
$ts = $item['ts'];
if (!array_key_exists($ts, $data0)) {
$data0[$ts] = [
'ts' => $ts,
$field1 => 0,
$field2 => 0,
$field3 => 0,
];
}
$data0[$ts][$field3] = $item[$field3];
}
// TODO: tmp order troubles fix
usort($data0, function ($a, $b) {
return $a['ts'] - $b['ts'];
});
return $data0;
}
protected function addEmptyDays(array $params): array {
$cnt = count($params);
$data = array_fill(0, $cnt, []);
$request = $this->f3->get('REQUEST');
$step = \Utils\Constants::get('CHART_RESOLUTION')[$this->getResolution($request)];
// use offset shift because $startTs/$endTs compared with shifted ['ts']
$offset = \Utils\TimeZones::getCurrentOperatorOffset();
$dateRange = $this->getDatesRange($request, $offset);
if (!$dateRange) {
$now = time() + $offset;
$week = 7 * 24 * 60 * 60;
if (count($params[0]) === 0) {
$dateRange = [
'endDate' => date('Y-m-d H:i:s', $now),
'startDate' => date('Y-m-d 00:00:01', $now - $week),
];
} else {
$firstTs = ($now - $params[0][0] < $week) ? $now - $week : $params[0][0];
$dateRange = [
'endDate' => date('Y-m-d H:i:s', $now),
'startDate' => date('Y-m-d 00:00:01', $firstTs),
];
}
}
$endTs = strtotime($dateRange['endDate']);
$startTs = strtotime($dateRange['startDate']);
$endTs = $endTs - ($endTs % $step);
$startTs = $startTs - ($startTs % $step);
$ox = $params[0];
while ($endTs >= $startTs) {
$itemIdx = array_search($startTs, $ox);
$data[0][] = $startTs;
for ($i = 1; $i < $cnt; ++$i) {
$data[$i][] = ($itemIdx !== false) ? $params[$i][$itemIdx] : 0;
}
$startTs += $step;
}
return $data;
}
protected function execute(string $query, int $apiKey): array {
$request = $this->f3->get('REQUEST');
// do not use offset because :start_time/:end_time compared with UTC db timestamps
$dateRange = $this->getDatesRange($request);
// Search request does not contain daterange param
if (!$dateRange) {
$dateRange = [
'endDate' => date('Y-m-d H:i:s'),
'startDate' => date('Y-m-d H:i:s', 0),
];
}
$offset = \Utils\TimeZones::getCurrentOperatorOffset();
$params = [
':api_key' => $apiKey,
':end_time' => $dateRange['endDate'],
':start_time' => $dateRange['startDate'],
':resolution' => $this->getResolution($request),
':offset' => strval($offset), // str for postgres
];
return $this->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,107 @@
<?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 Models\Chart;
class BaseEventsCount extends \Models\BaseSql {
use \Traits\DateRange;
protected $DB_TABLE_NAME = 'event';
protected $alertTypesParams;
protected $editTypesParams;
protected $normalTypesParams;
protected $alertFlatIds;
protected $editFlatIds;
protected $normalFlatIds;
public function __construct() {
parent::__construct();
[$this->alertTypesParams, $this->alertFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('ALERT_EVENT_TYPES'), 'alert');
[$this->editTypesParams, $this->editFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('EDITING_EVENT_TYPES'), 'edit');
[$this->normalTypesParams, $this->normalFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('NORMAL_EVENT_TYPES'), 'normal');
}
public function getData(int $apiKey): array {
$itemsByDate = [];
$items = $this->getCounts($apiKey);
foreach ($items as $item) {
$itemsByDate[$item['ts']] = [
$item['event_normal_type_count'],
$item['event_editing_type_count'],
$item['event_alert_type_count'],
];
}
$request = $this->f3->get('REQUEST');
// use offset shift because $startTs/$endTs compared with shifted ['ts']
$offset = \Utils\TimeZones::getCurrentOperatorOffset();
$datesRange = $this->getLatestNDatesRange(180, $offset);
$endTs = strtotime($datesRange['endDate']);
$startTs = strtotime($datesRange['startDate']);
$step = \Utils\Constants::get('CHART_RESOLUTION')[$this->getResolution($request)];
$endTs = $endTs - ($endTs % $step);
$startTs = $startTs - ($startTs % $step);
while ($endTs >= $startTs) {
if (!isset($itemsByDate[$startTs])) {
$itemsByDate[$startTs] = [null, null, null];
}
$startTs += $step;
}
ksort($itemsByDate);
$ox = [];
$l1 = [];
$l2 = [];
$l3 = [];
foreach ($itemsByDate as $key => $value) {
$ox[] = $key;
$l1[] = $value[0];
$l2[] = $value[1];
$l3[] = $value[2];
}
return [$ox, $l1, $l2, $l3];
}
protected function executeOnRangeById(string $query, int $apiKey): array {
$request = $this->f3->get('REQUEST');
// do not use offset because :start_time/:end_time compared with UTC event.time
$dateRange = $this->getLatestNDatesRange(180);
$offset = \Utils\TimeZones::getCurrentOperatorOffset();
$params = [
':api_key' => $apiKey,
':end_time' => $dateRange['endDate'],
':start_time' => $dateRange['startDate'],
':resolution' => $this->getResolution($request),
':id' => $request['id'],
':offset' => strval($offset), // str for postgres
];
$params = array_merge($params, $this->alertTypesParams);
$params = array_merge($params, $this->editTypesParams);
$params = array_merge($params, $this->normalTypesParams);
return $this->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,96 @@
<?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 Models\Chart;
class Blacklist extends Base {
public function getData(int $apiKey): array {
$data = $this->getFirstLine($apiKey);
$timestamps = array_column($data, 'ts');
$line1 = array_column($data, 'ts_new_records');
return $this->addEmptyDays([$timestamps, $line1]);
}
private function getFirstLine(int $apiKey): array {
$query = (
'SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, tbl.created + :offset))::bigint AS ts,
COUNT(*) AS ts_new_records
FROM (
SELECT DISTINCT
blacklist.accountid,
blacklist.created,
extra.type,
CASE extra.type
WHEN \'ip\' THEN blacklist.ip
WHEN \'email\' THEN blacklist.email
WHEN \'phone\' THEN blacklist.phone
END AS value
FROM
(
SELECT
event_account.id AS accountid,
event_account.latest_decision AS created,
CASE WHEN event_ip.fraud_detected THEN split_part(event_ip.ip::text, \'/\', 1) ELSE NULL END AS ip,
event_ip.fraud_detected AS ip_fraud,
CASE WHEN event_email.fraud_detected THEN event_email.email ELSE NULL END AS email,
event_email.fraud_detected AS email_fraud,
CASE WHEN event_phone.fraud_detected THEN event_phone.phone_number ELSE NULL END AS phone,
event_phone.fraud_detected AS phone_fraud
FROM event
LEFT JOIN event_account
ON event_account.id = event.account
LEFT JOIN event_ip
ON event_ip.id = event.ip
LEFT JOIN event_email
ON event_email.id = event.email
LEFT JOIN event_phone
ON event_phone.id = event.phone
WHERE
event_account.key = :api_key AND
event_account.fraud IS TRUE AND
event_account.latest_decision >= :start_time AND
event_account.latest_decision <= :end_time AND
(
event_email.fraud_detected IS TRUE OR
event_ip.fraud_detected IS TRUE OR
event_phone.fraud_detected IS TRUE
)
) AS blacklist,
LATERAL (
VALUES
(CASE WHEN ip_fraud = true THEN \'ip\' END),
(CASE WHEN email_fraud = true THEN \'email\' END),
(CASE WHEN phone_fraud = true THEN \'phone\' END)
) AS extra(type)
WHERE
extra.type IS NOT NULL
) AS tbl
GROUP BY ts
ORDER BY ts'
);
return $this->execute($query, $apiKey);
}
}

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)
*/
namespace Models\Chart;
class Bot extends BaseEventsCount {
public function getCounts(int $apiKey): array {
$query = (
"SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts,
COUNT(CASE WHEN event.type IN ({$this->normalFlatIds}) THEN TRUE END) AS event_normal_type_count,
COUNT(CASE WHEN event.type IN ({$this->editFlatIds}) THEN TRUE END) AS event_editing_type_count,
COUNT(CASE WHEN event.type IN ({$this->alertFlatIds}) THEN TRUE END) AS event_alert_type_count
FROM
event
INNER JOIN event_device
ON (event.device = event_device.id)
INNER JOIN event_ua_parsed
ON (event_device.user_agent = event_ua_parsed.id)
WHERE
event_ua_parsed.id = :id AND
event.key = :api_key AND
event.time >= :start_time AND
event.time <= :end_time
GROUP BY ts
ORDER BY ts"
);
return $this->executeOnRangeById($query, $apiKey);
}
}

View File

@@ -0,0 +1,56 @@
<?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 Models\Chart;
class Bots extends Base {
protected $DB_TABLE_NAME = 'event_device';
public function getData(int $apiKey): array {
$data = $this->getFirstLine($apiKey);
$ox = array_column($data, 'ts');
$l1 = array_column($data, 'bot_count');
return $this->addEmptyDays([$ox, $l1]);
}
private function getFirstLine(int $apiKey): array {
$query = (
'SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts,
COUNT(DISTINCT (
CASE WHEN event_ua_parsed.modified IS TRUE THEN event.device END)
) AS bot_count
FROM
event
INNER JOIN event_device
ON(event.device = event_device.id)
INNER JOIN event_ua_parsed
ON(event_device.user_agent=event_ua_parsed.id)
WHERE
event.key = :api_key AND
event.time >= :start_time AND
event.time <= :end_time
GROUP BY ts
ORDER BY ts'
);
return $this->execute($query, $apiKey);
}
}

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)
*/
namespace Models\Chart;
class Country extends BaseEventsCount {
public function getCounts(int $apiKey): array {
$query = (
"SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts,
COUNT(CASE WHEN event.type IN ({$this->normalFlatIds}) THEN TRUE END) AS event_normal_type_count,
COUNT(CASE WHEN event.type IN ({$this->editFlatIds}) THEN TRUE END) AS event_editing_type_count,
COUNT(CASE WHEN event.type IN ({$this->alertFlatIds}) THEN TRUE END) AS event_alert_type_count
FROM
event
INNER JOIN event_ip
ON (event.ip = event_ip.id)
INNER JOIN countries
ON (event_ip.country = countries.id)
WHERE
countries.id = :id AND
event.key = :api_key AND
event.time >= :start_time AND
event.time <= :end_time
GROUP BY ts
ORDER BY ts"
);
return $this->executeOnRangeById($query, $apiKey);
}
}

View File

@@ -0,0 +1,45 @@
<?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 Models\Chart;
class Domain extends BaseEventsCount {
public function getCounts(int $apiKey): array {
$query = (
"SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts,
COUNT(CASE WHEN event.type IN ({$this->normalFlatIds}) THEN TRUE END) AS event_normal_type_count,
COUNT(CASE WHEN event.type IN ({$this->editFlatIds}) THEN TRUE END) AS event_editing_type_count,
COUNT(CASE WHEN event.type IN ({$this->alertFlatIds}) THEN TRUE END) AS event_alert_type_count
FROM
event
INNER JOIN event_email
ON (event.email = event_email.id)
WHERE
event_email.domain = :id AND
event.key = :api_key AND
event.time >= :start_time AND
event.time <= :end_time
GROUP BY ts
ORDER BY ts"
);
return $this->executeOnRangeById($query, $apiKey);
}
}

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)
*/
namespace Models\Chart;
class Domains extends Base {
protected $DB_TABLE_NAME = 'event';
public function getData(int $apiKey): array {
$field1 = 'unique_domains_count';
$data1 = $this->getFirstLine($apiKey);
$field2 = 'ts_new_domains';
$data2 = $this->getSecondLine($apiKey);
$data0 = $this->concatDataLines($data1, $field1, $data2, $field2);
$indexedData = array_values($data0);
$timestamps = array_column($indexedData, 'ts');
$line1 = array_column($indexedData, $field1);
$line2 = array_column($indexedData, $field2);
return $this->addEmptyDays([$timestamps, $line1, $line2]);
}
private function getFirstLine(int $apiKey): array {
$query = (
'SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts,
COUNT(DISTINCT event_email.domain) AS unique_domains_count
FROM
event
INNER JOIN event_email
ON (event.email = event_email.id)
WHERE
event.key = :api_key AND
event.time >= :start_time AND
event.time <= :end_time
GROUP BY ts
ORDER BY ts'
);
return $this->execute($query, $apiKey);
}
private function getSecondLine(int $apiKey): array {
$query = (
'SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event_domain.created + :offset))::bigint AS ts,
COUNT(event_domain.id) AS ts_new_domains
FROM
event_domain
WHERE
event_domain.key = :api_key AND
event_domain.created >= :start_time AND
event_domain.created <= :end_time
GROUP BY ts
ORDER BY ts'
);
return $this->execute($query, $apiKey, false);
}
}

View File

@@ -0,0 +1,56 @@
<?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 Models\Chart;
class Emails extends Base {
protected $DB_TABLE_NAME = 'event';
public function getData(int $apiKey): array {
$data = $this->getFirstLine($apiKey);
$timestamps = array_column($data, 'ts');
$line1 = array_column($data, 'email_count');
$line2 = array_column($data, 'blockemails_count');
return $this->addEmptyDays([$timestamps, $line1, $line2]);
}
private function getFirstLine(int $apiKey): array {
$query = (
'SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts,
COUNT(DISTINCT event.email) AS email_count,
COUNT(DISTINCT (
CASE WHEN event_email.blockemails IS TRUE THEN event.email END)
) AS blockemails_count
FROM
event
INNER JOIN event_email
ON (event.email = event_email.id)
WHERE
event.key = :api_key AND
event.time >= :start_time AND
event.time <= :end_time
GROUP BY ts
ORDER BY ts'
);
return $this->execute($query, $apiKey);
}
}

View File

@@ -0,0 +1,83 @@
<?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 Models\Chart;
class Events extends Base {
protected $DB_TABLE_NAME = 'event';
public function getData(int $apiKey): array {
$data = $this->getFirstLine($apiKey);
$timestamps = array_column($data, 'ts');
$line1 = array_column($data, 'event_normal_type_count');
$line2 = array_column($data, 'event_editing_type_count');
$line3 = array_column($data, 'event_alert_type_count');
$line4 = array_column($data, 'unauthorized_event_count');
return $this->addEmptyDays([$timestamps, $line1, $line2, $line3, $line4]);
}
private function getFirstLine(int $apiKey): array {
$request = $this->f3->get('REQUEST');
$dateRange = $this->getDatesRange($request);
if (!$dateRange) {
$dateRange = [
'endDate' => date('Y-m-d H:i:s'),
'startDate' => date('Y-m-d H:i:s', 0),
];
}
$offset = \Utils\TimeZones::getCurrentOperatorOffset();
[$alertTypesParams, $alertFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('ALERT_EVENT_TYPES'), 'alert');
[$editTypesParams, $editFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('EDITING_EVENT_TYPES'), 'edit');
[$normalTypesParams, $normalFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('NORMAL_EVENT_TYPES'), 'normal');
$params = [
':api_key' => $apiKey,
':end_time' => $dateRange['endDate'],
':start_time' => $dateRange['startDate'],
':resolution' => $this->getResolution($request),
':offset' => strval($offset),
':unauth' => \Utils\Constants::get('UNAUTHORIZED_USERID'),
];
$params = array_merge($params, $alertTypesParams);
$params = array_merge($params, $editTypesParams);
$params = array_merge($params, $normalTypesParams);
$query = (
"SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts,
COUNT(CASE WHEN event.type IN ({$normalFlatIds}) THEN TRUE END) AS event_normal_type_count,
COUNT(CASE WHEN event.type IN ({$editFlatIds}) THEN TRUE END) AS event_editing_type_count,
COUNT(CASE WHEN event.type IN ({$alertFlatIds}) THEN TRUE END) AS event_alert_type_count,
COUNT(CASE WHEN event_account.userid = :unauth THEN TRUE END) AS unauthorized_event_count
FROM
event
LEFT JOIN event_account
ON event.account = event_account.id
WHERE
event.key = :api_key AND
event.time >= :start_time AND
event.time <= :end_time
GROUP BY ts
ORDER BY ts"
);
return $this->execQuery($query, $params);
}
}

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)
*/
namespace Models\Chart;
class Ip extends BaseEventsCount {
public function getCounts(int $apiKey): array {
$query = (
"SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts,
COUNT(CASE WHEN event.type IN ({$this->normalFlatIds}) THEN TRUE END) AS event_normal_type_count,
COUNT(CASE WHEN event.type IN ({$this->editFlatIds}) THEN TRUE END) AS event_editing_type_count,
COUNT(CASE WHEN event.type IN ({$this->alertFlatIds}) THEN TRUE END) AS event_alert_type_count
FROM
event
WHERE
event.ip = :id AND
event.key = :api_key AND
event.time >= :start_time AND
event.time <= :end_time
GROUP BY ts
ORDER BY ts"
);
return $this->executeOnRangeById($query, $apiKey);
}
}

View File

@@ -0,0 +1,84 @@
<?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 Models\Chart;
class Ips extends Base {
protected $DB_TABLE_NAME = 'event';
public function getData(int $apiKey): array {
$data = $this->getFirstLine($apiKey);
$timestamps = array_column($data, 'ts');
$line1 = array_column($data, 'residence_ip_count');
$line2 = array_column($data, 'total_privacy');
$line3 = array_column($data, 'suspicious_ip_count');
return $this->addEmptyDays([$timestamps, $line1, $line2, $line3]);
}
private function getFirstLine(int $apiKey): array {
$query = (
'SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts,
COUNT(DISTINCT event.ip) AS unique_ip_count,
COUNT(DISTINCT
CASE
WHEN event_ip.data_center IS TRUE OR
event_ip.tor IS TRUE OR
event_ip.vpn IS TRUE
THEN event.ip
ELSE NULL
END
) AS total_privacy,
COUNT(DISTINCT event.ip) - COUNT(DISTINCT
CASE
WHEN event_ip.data_center IS TRUE OR
event_ip.tor IS TRUE OR
event_ip.vpn IS TRUE
THEN event.ip
ELSE NULL
END
) AS residence_ip_count,
COUNT(DISTINCT
CASE
WHEN event_ip.blocklist IS TRUE OR
event_ip.fraud_detected IS TRUE
THEN event.ip
ELSE NULL
END
) AS suspicious_ip_count
FROM
event
INNER JOIN event_ip
ON (event.ip = event_ip.id)
WHERE
event.key = :api_key AND
event.time >= :start_time AND
event.time <= :end_time
GROUP BY ts
ORDER BY ts'
);
return $this->execute($query, $apiKey);
}
}

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)
*/
namespace Models\Chart;
class Isp extends BaseEventsCount {
public function getCounts(int $apiKey): array {
$query = (
"SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts,
COUNT(CASE WHEN event.type IN ({$this->normalFlatIds}) THEN TRUE END) AS event_normal_type_count,
COUNT(CASE WHEN event.type IN ({$this->editFlatIds}) THEN TRUE END) AS event_editing_type_count,
COUNT(CASE WHEN event.type IN ({$this->alertFlatIds}) THEN TRUE END) AS event_alert_type_count
FROM
event
INNER JOIN event_ip
ON (event.ip = event_ip.id)
LEFT JOIN event_isp
ON (event_ip.isp = event_isp.id)
WHERE
event_isp.id = :id AND
event.key = :api_key AND
event.time >= :start_time AND
event.time <= :end_time
GROUP BY ts
ORDER BY ts"
);
return $this->executeOnRangeById($query, $apiKey);
}
}

View File

@@ -0,0 +1,83 @@
<?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 Models\Chart;
class Isps extends Base {
protected $DB_TABLE_NAME = 'event';
public function getData(int $apiKey): array {
$field1 = 'unique_isps_count';
$data1 = $this->getFirstLine($apiKey);
$field2 = 'ts_new_isps';
$data2 = $this->getSecondLine($apiKey);
$data0 = $this->concatDataLines($data1, $field1, $data2, $field2);
$indexedData = array_values($data0);
$timestamps = array_column($indexedData, 'ts');
$line1 = array_column($indexedData, $field1);
$line2 = array_column($indexedData, $field2);
return $this->addEmptyDays([$timestamps, $line1, $line2]);
}
private function getFirstLine(int $apiKey): array {
$query = (
'SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts,
COUNT(DISTINCT event_isp.id) AS unique_isps_count
FROM
event
INNER JOIN event_ip
ON (event.ip = event_ip.id)
LEFT JOIN event_isp
ON (event_ip.isp = event_isp.id)
WHERE
event.key = :api_key AND
event.time >= :start_time AND
event.time <= :end_time
GROUP BY ts
ORDER BY ts'
);
return $this->execute($query, $apiKey);
}
private function getSecondLine(int $apiKey): array {
$query = (
'SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event_isp.created + :offset))::bigint AS ts,
COUNT(event_isp.id) AS ts_new_isps
FROM
event_isp
WHERE
event_isp.key = :api_key AND
event_isp.created >= :start_time AND
event_isp.created <= :end_time
GROUP BY ts
ORDER BY ts'
);
return $this->execute($query, $apiKey, false);
}
}

View File

@@ -0,0 +1,82 @@
<?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 Models\Chart;
class Logbook extends Base {
protected $DB_TABLE_NAME = 'event_logbook';
public function getData(int $apiKey): array {
$data = $this->getFirstLine($apiKey);
$timestamps = array_column($data, 'ts');
$line1 = array_column($data, 'event_normal_type_count');
$line2 = array_column($data, 'event_issued_type_count');
$line3 = array_column($data, 'event_failed_type_count');
return $this->addEmptyDays([$timestamps, $line1, $line2, $line3]);
}
private function getFirstLine(int $apiKey): array {
$request = $this->f3->get('REQUEST');
$dateRange = $this->getDatesRange($request);
if (!$dateRange) {
$dateRange = [
'endDate' => date('Y-m-d H:i:s'),
'startDate' => date('Y-m-d H:i:s', 0),
];
}
//$dateRange['endDate'] = \Utils\TimeZones::localizeForActiveOperator($dateRange['endDate']);
//$dateRange['startDate'] = \Utils\TimeZones::localizeForActiveOperator($dateRange['startDate']);
[$failedTypesParams, $failedFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('FAILED_LOGBOOK_EVENT_TYPES'), 'failed');
[$issuedTypesParams, $issuedFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('ISSUED_LOGBOOK_EVENT_TYPES'), 'issued');
[$normalTypesParams, $normalFlatIds] = $this->getArrayPlaceholders(\Utils\Constants::get('NORMAL_LOGBOOK_EVENT_TYPES'), 'normal');
$params = [
':api_key' => $apiKey,
':end_time' => $dateRange['endDate'],
':start_time' => $dateRange['startDate'],
':resolution' => $this->getResolution($request),
];
$params = array_merge($params, $failedTypesParams);
$params = array_merge($params, $issuedTypesParams);
$params = array_merge($params, $normalTypesParams);
$query = (
"SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event_logbook.started))::bigint AS ts,
COUNT(CASE WHEN event_error_type.value IN ({$normalFlatIds}) THEN TRUE END) AS event_normal_type_count,
COUNT(CASE WHEN event_error_type.value IN ({$issuedFlatIds}) THEN TRUE END) AS event_issued_type_count,
COUNT(CASE WHEN event_error_type.value IN ({$failedFlatIds}) THEN TRUE END) AS event_failed_type_count
FROM
event_logbook
LEFT JOIN event_error_type
ON event_logbook.error_type = event_error_type.id
WHERE
event_logbook.key = :api_key AND
event_logbook.started >= :start_time AND
event_logbook.started <= :end_time
GROUP BY ts
ORDER BY ts"
);
return $this->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,49 @@
<?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 Models\Chart;
class Phones extends Base {
protected $DB_TABLE_NAME = 'event';
public function getData(int $apiKey): array {
$data = $this->getFirstLine($apiKey);
$timestamps = array_column($data, 'ts');
$line1 = array_column($data, 'phone_count');
return $this->addEmptyDays([$timestamps, $line1]);
}
private function getFirstLine(int $apiKey): array {
$query = (
'SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event_phone.lastseen + :offset))::bigint AS ts,
COUNT(*) AS phone_count
FROM
event_phone
WHERE
event_phone.key = :api_key AND
event_phone.lastseen >= :start_time AND
event_phone.lastseen <= :end_time
GROUP BY ts
ORDER BY ts'
);
return $this->execute($query, $apiKey);
}
}

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)
*/
namespace Models\Chart;
class Resource extends BaseEventsCount {
public function getCounts(int $apiKey): array {
$query = (
"SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts,
COUNT(CASE WHEN event.type IN ({$this->normalFlatIds}) THEN TRUE END) AS event_normal_type_count,
COUNT(CASE WHEN event.type IN ({$this->editFlatIds}) THEN TRUE END) AS event_editing_type_count,
COUNT(CASE WHEN event.type IN ({$this->alertFlatIds}) THEN TRUE END) AS event_alert_type_count
FROM
event
WHERE
event.url = :id AND
event.key = :api_key AND
event.time >= :start_time AND
event.time <= :end_time
GROUP BY ts
ORDER BY ts"
);
return $this->executeOnRangeById($query, $apiKey);
}
}

View File

@@ -0,0 +1,64 @@
<?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 Models\Chart;
class Resources extends Base {
protected $DB_TABLE_NAME = 'event';
public function getData(int $apiKey): array {
$data = $this->getFirstLine($apiKey);
$timestamps = array_column($data, 'ts');
$line1 = array_column($data, 'count_200');
$line2 = array_column($data, 'count_404');
$line3 = array_column($data, 'count_500');
return $this->addEmptyDays([$timestamps, $line1, $line2, $line3]);
}
private function getFirstLine(int $apiKey): array {
$query = (
'SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts,
COUNT(DISTINCT event.id) AS url_count,
COUNT(DISTINCT (
CASE WHEN event.http_code=200 OR event.http_code IS NULL THEN event.id END)
) AS count_200,
COUNT(DISTINCT (
CASE WHEN event.http_code = 404 THEN event.id END)
) AS count_404,
COUNT(DISTINCT (
CASE WHEN event.http_code IN(403, 500) THEN event.id END)
) AS count_500
FROM
event
WHERE
event.key = :api_key AND
event.time >= :start_time AND
event.time <= :end_time
GROUP BY ts
ORDER BY ts'
);
return $this->execute($query, $apiKey);
}
}

View File

@@ -0,0 +1,106 @@
<?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 Models\Chart;
class ReviewQueue extends Base {
use \Traits\DateRange;
protected $DB_TABLE_NAME = 'event';
public function getData(int $apiKey): array {
$field1 = 'ts_new_users_whitelisted';
$data1 = $this->getFirstLine($apiKey);
$field2 = 'ts_new_added_to_review';
$data2 = $this->getSecondLine($apiKey);
$field3 = 'ts_new_users_blacklisted';
$data3 = $this->getThirdLine($apiKey);
$data0 = $this->concatDataLines($data1, $field1, $data2, $field2, $data3, $field3);
$indexedData = array_values($data0);
$timestamps = array_column($indexedData, 'ts');
$line1 = array_column($indexedData, $field1);
$line2 = array_column($indexedData, $field2);
$line3 = array_column($indexedData, $field3);
return $this->addEmptyDays([$timestamps, $line1, $line2, $line3]);
}
private function getFirstLine(int $apiKey): array {
$query = (
'SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event_account.latest_decision + :offset))::bigint AS ts,
COUNT(event_account.id) as ts_new_users_whitelisted
FROM
event_account
WHERE
event_account.key = :api_key AND
event_account.fraud IS FALSE AND
event_account.latest_decision >= :start_time AND
event_account.latest_decision <= :end_time
GROUP BY ts
ORDER BY ts'
);
return $this->execute($query, $apiKey, false);
}
private function getSecondLine(int $apiKey): array {
$query = (
'SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event_account.added_to_review + :offset))::bigint AS ts,
COUNT(event_account.id) AS ts_new_added_to_review
FROM
event_account
WHERE
event_account.key = :api_key AND
event_account.added_to_review IS NOT NULL AND
event_account.added_to_review >= :start_time AND
event_account.added_to_review <= :end_time
GROUP BY ts
ORDER BY ts'
);
return $this->execute($query, $apiKey, false);
}
private function getThirdLine(int $apiKey): array {
$query = (
'SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event_account.latest_decision + :offset))::bigint AS ts,
COUNT(event_account.id) as ts_new_users_blacklisted
FROM
event_account
WHERE
event_account.key = :api_key AND
event_account.fraud IS TRUE AND
event_account.latest_decision >= :start_time AND
event_account.latest_decision <= :end_time
GROUP BY ts
ORDER BY ts'
);
return $this->execute($query, $apiKey, false);
}
}

View File

@@ -0,0 +1,150 @@
<?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 Models\Chart;
class SessionStat extends Base {
protected $DB_TABLE_NAME = 'event_session';
public function getData(int $apiKey): array {
$itemsByDate = [];
$items = $this->getCounts($apiKey);
foreach ($items as $item) {
$itemsByDate[$item['ts']] = [
$item['new_device_count_sum'],
$item['new_ip_count_sum'],
$item['event_session_cnt'],
$item['event_count_max'],
];
}
$request = $this->f3->get('REQUEST');
// use offset shift because $startTs/$endTs compared with shifted ['ts']
$offset = \Utils\TimeZones::getCurrentOperatorOffset();
$datesRange = $this->getLatestNDatesRange(14, $offset);
$endTs = strtotime($datesRange['endDate']);
$startTs = strtotime($datesRange['startDate']);
$step = \Utils\Constants::get('CHART_RESOLUTION')[$this->getResolution($request)];
$endTs = $endTs - ($endTs % $step);
$startTs = $startTs - ($startTs % $step);
$midTs = $startTs + $step * 7;
foreach ($itemsByDate as $ts => $value) {
if ($ts <= $midTs) {
$newTs = $ts + $step * 7;
if (!array_key_exists($newTs, $itemsByDate)) {
$itemsByDate[$newTs] = [
0, 0, 0, 0,
$value[0], $value[1], $value[2], $value[3],
];
} else {
$itemsByDate[$newTs][] = $value[0];
$itemsByDate[$newTs][] = $value[1];
$itemsByDate[$newTs][] = $value[2];
$itemsByDate[$newTs][] = $value[3];
}
}
}
while ($endTs >= $startTs) {
if (!isset($itemsByDate[$startTs]) && $startTs > $midTs) {
$itemsByDate[$startTs] = [0, 0, 0, 0, 0, 0, 0, 0];
} elseif (isset($itemsByDate[$startTs]) && count($itemsByDate[$startTs]) === 4) {
$itemsByDate[$startTs][] = 0;
$itemsByDate[$startTs][] = 0;
$itemsByDate[$startTs][] = 0;
$itemsByDate[$startTs][] = 0;
}
$startTs += $step;
}
foreach (array_keys($itemsByDate) as $key) {
if ($key <= $midTs) {
unset($itemsByDate[$key]);
}
}
ksort($itemsByDate);
$result = [array_keys($itemsByDate)];
for ($i = 0; $i < 8; ++$i) {
$result[] = array_column($itemsByDate, $i);
}
return $result;
}
protected function executeOnRangeById(string $query, int $apiKey): array {
$request = $this->f3->get('REQUEST');
// do not use offset because :start_time/:end_time compared with UTC event.time
$dateRange = $this->getLatestNDatesRange(14);
$offset = \Utils\TimeZones::getCurrentOperatorOffset();
$params = [
':api_key' => $apiKey,
':end_time' => $dateRange['endDate'],
':start_time' => $dateRange['startDate'],
//':resolution' => $this->getResolution($request),
':resolution' => 60 * 60 * 24,
':id' => $request['id'],
':offset' => strval($offset), // str for postgres
];
return $this->execQuery($query, $params);
}
private function getCounts(int $apiKey): array {
$query = (
'SELECT
((EXTRACT(EPOCH FROM event_session.created)::bigint + :offset::bigint) / :resolution) * :resolution as ts,
COUNT(event_session.id) AS event_session_cnt,
MAX(event_session_stat.event_count) AS event_count_max,
FLOOR(AVG(event_session_stat.event_count))::int AS event_count_avg,
MAX(event_session_stat.device_count) AS device_count_max,
FLOOR(AVG(event_session_stat.device_count))::int AS device_count_avg,
MAX(event_session_stat.ip_count) AS ip_count_max,
FLOOR(AVG(event_session_stat.ip_count))::int AS ip_count_avg,
MAX(event_session_stat.country_count) AS country_count_max,
FLOOR(AVG(event_session_stat.country_count))::int AS country_count_avg,
SUM(event_session_stat.new_ip_count) AS new_ip_count_sum,
SUM(event_session_stat.new_device_count) AS new_device_count_sum,
jsonb_agg(event_session_stat.event_types) AS event_types,
jsonb_agg(event_session_stat.http_codes) AS http_codes
FROM
event_session
LEFT JOIN event_session_stat
ON event_session.id = event_session_stat.session_id
WHERE
event_session.account_id = :id AND
event_session.key = :api_key AND
event_session.created >= :start_time AND
event_session.created <= :end_time
GROUP BY ts
ORDER BY ts'
);
return $this->executeOnRangeById($query, $apiKey);
}
}

View File

@@ -0,0 +1,45 @@
<?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 Models\Chart;
class User extends BaseEventsCount {
public function getCounts(int $apiKey) {
$query = (
"SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event.time + :offset))::bigint AS ts,
COUNT(CASE WHEN event.type IN ({$this->normalFlatIds}) THEN TRUE END) AS event_normal_type_count,
COUNT(CASE WHEN event.type IN ({$this->editFlatIds}) THEN TRUE END) AS event_editing_type_count,
COUNT(CASE WHEN event.type IN ({$this->alertFlatIds}) THEN TRUE END) AS event_alert_type_count
FROM
event
INNER JOIN event_account
ON (event.account = event_account.id)
WHERE
event_account.id = :id AND
event.key = :api_key AND
event.time >= :start_time AND
event.time <= :end_time
GROUP BY ts
ORDER BY ts"
);
return $this->executeOnRangeById($query, $apiKey);
}
}

View File

@@ -0,0 +1,89 @@
<?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 Models\Chart;
class Users extends Base {
use \Traits\DateRange;
protected $DB_TABLE_NAME = 'event_account';
public function getData(int $apiKey): array {
$data0 = [];
$data1 = $this->getFirstLine($apiKey);
$iters = count($data1);
for ($i = 0; $i < $iters; ++$i) {
$item = $data1[$i];
$ts = $item['ts'];
$score = $item['score'];
if (!isset($data0[$ts])) {
$data0[$ts] = [
'ts' => $ts,
'ts_new_users_with_trust_score_high' => 0,
'ts_new_users_with_trust_score_medium' => 0,
'ts_new_users_with_trust_score_low' => 0,
];
}
$inf = \Utils\Constants::get('USER_HIGH_SCORE_INF');
if ($score >= \Utils\Constants::get('USER_HIGH_SCORE_INF')) {
++$data0[$ts]['ts_new_users_with_trust_score_high'];
}
$inf = \Utils\Constants::get('USER_MEDIUM_SCORE_INF');
$sup = \Utils\Constants::get('USER_MEDIUM_SCORE_SUP');
if ($score >= $inf && $score < $sup) {
++$data0[$ts]['ts_new_users_with_trust_score_medium'];
}
$inf = \Utils\Constants::get('USER_LOW_SCORE_INF');
$sup = \Utils\Constants::get('USER_LOW_SCORE_SUP');
if ($score >= $inf && $score < $sup) {
++$data0[$ts]['ts_new_users_with_trust_score_low'];
}
}
$indexedData = array_values($data0);
$timestamps = array_column($indexedData, 'ts');
$line1 = array_column($indexedData, 'ts_new_users_with_trust_score_high');
$line2 = array_column($indexedData, 'ts_new_users_with_trust_score_medium');
$line3 = array_column($indexedData, 'ts_new_users_with_trust_score_low');
return $this->addEmptyDays([$timestamps, $line1, $line2, $line3]);
}
private function getFirstLine(int $apiKey) {
$query = (
'SELECT
EXTRACT(EPOCH FROM date_trunc(:resolution, event_account.created + :offset))::bigint AS ts,
event_account.id,
event_account.score
FROM
event_account
WHERE
event_account.key = :api_key AND
event_account.created >= :start_time AND
event_account.created <= :end_time
GROUP BY ts, event_account.id
ORDER BY ts'
);
return $this->execute($query, $apiKey);
}
}

View File

@@ -0,0 +1,61 @@
<?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 Models\Chart;
class Watchlist extends Base {
private $userIds = [];
protected $DB_TABLE_NAME = 'event';
public function getData(int $apiKey): array {
$params = $this->getRequestParams($apiKey);
$params[':users_ids'] = $this->userIds;
$query = (
"SELECT
TEXT(date_trunc('day', event.time)) AS day,
COUNT(event.id) AS event_count
FROM
event
INNER JOIN event_account
ON (event.account = event_account.id)
INNER JOIN event_url
ON (event.url = event_url.id)
INNER JOIN event_ip
ON (event.ip = event_ip.id)
INNER JOIN countries
ON (event_ip.country = countries.id)
WHERE
event.key = :api_key
%s
GROUP BY day
ORDER BY day"
);
//$request = $this->f3->get('REQUEST');
//$dateRange = $this->getDatesRange($request);
return $this->execQuery($query, $params);
}
public function setUsersIds(array $userIds): void {
$this->userIds = $userIds;
}
}

View File

@@ -0,0 +1,53 @@
<?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 Models\Context;
abstract class Base extends \Models\BaseSql {
protected $DB_TABLE_NAME = 'event';
abstract public function getContext(array $accountIds, int $apiKey): array;
abstract protected function getDetails(array $accountIds, int $apiKey): array;
protected function getRequestParams(array $accountIds, int $apiKey): array {
[$params, $placeHolders] = $this->getArrayPlaceholders($accountIds);
$params[':api_key'] = $apiKey;
return [$params, $placeHolders];
}
protected function groupRecordsByAccount(array $records): array {
$recordsByAccount = [];
$iters = count($records);
for ($i = 0; $i < $iters; ++$i) {
$item = $records[$i];
$accountId = $item['accountid'];
if (!isset($recordsByAccount[$accountId])) {
$recordsByAccount[$accountId] = [];
}
$recordsByAccount[$accountId][] = $item;
}
return $recordsByAccount;
}
protected function getUniqueArray(array $array): array {
return array_values(array_unique($array));
}
}

View File

@@ -0,0 +1,353 @@
<?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 Models\Context;
class Data {
private \Models\Context\User $userModel;
private \Models\Context\Ip $ipModel;
private \Models\Context\Device $deviceModel;
private \Models\Context\Email $emailModel;
private \Models\Context\Phone $phoneModel;
private \Models\Context\Event $eventModel;
private \Models\Context\Session $sessionModel;
private \Models\ApiKeys $keyModel;
private array $suspiciousWordsUrl;
private array $suspiciousWordsUserAgent;
private array $suspiciousWordsEmail;
public function __construct() {
$this->userModel = new User();
$this->ipModel = new Ip();
$this->deviceModel = new Device();
$this->emailModel = new Email();
$this->phoneModel = new Phone();
$this->eventModel = new Event();
$this->sessionModel = new Session();
$this->keyModel = new \Models\ApiKeys();
$this->suspiciousWordsUrl = \Utils\WordsLists\Url::getWords();
$this->suspiciousWordsUserAgent = \Utils\WordsLists\UserAgent::getWords();
$this->suspiciousWordsEmail = \Utils\WordsLists\Email::getWords();
}
public function getContext(array $accountIds, int $apiKey): array {
$userDetails = $this->userModel->getContext($accountIds, $apiKey);
$ipDetails = $this->ipModel->getContext($accountIds, $apiKey);
$deviceDetails = $this->deviceModel->getContext($accountIds, $apiKey);
$emailDetails = $this->emailModel->getContext($accountIds, $apiKey);
$phoneDetails = $this->phoneModel->getContext($accountIds, $apiKey);
$eventDetails = $this->eventModel->getContext($accountIds, $apiKey);
//$domainDetails = $this->domainModel->getContext($accountIds, $apiKey);
$timezoneName = $this->keyModel->getTimezoneByKeyId($apiKey);
$utcTime = new \DateTime('now', new \DateTimeZone('UTC'));
$timezone = new \DateTimeZone($timezoneName);
$offsetInSeconds = $timezone->getOffset($utcTime);
// get only suspicious sessions
$sessionDetails = $this->sessionModel->getContext($accountIds, $apiKey, $offsetInSeconds);
//Extend user details context
foreach ($userDetails as $userId => $user) {
$user['le_exists'] = ($user['le_email'] ?? null) !== null;
$user['le_email'] = $user['le_email'] ?? '';
$user['le_local_part'] = explode('@', $user['le_email'])[0] ?? '';
$user['le_domain_part'] = explode('@', $user['le_email'])[1] ?? '';
$userId = $user['ea_id'];
$ip = $ipDetails[$userId] ?? [];
$device = $deviceDetails[$userId] ?? [];
$email = $emailDetails[$userId] ?? [];
$phone = $phoneDetails[$userId] ?? [];
$events = $eventDetails[$userId] ?? [];
$session = $sessionDetails[$userId] ?? [];
$user['eip_ip_id'] = $ip['eip_ip_id'] ?? [];
$user['eip_ip'] = $ip['eip_ip'] ?? [];
$user['eip_cidr'] = $ip['eip_cidr'] ?? [];
$user['eip_country_id'] = $ip['eip_country_id'] ?? [];
$user['eip_data_center'] = $ip['eip_data_center'] ?? [];
$user['eip_tor'] = $ip['eip_tor'] ?? [];
$user['eip_vpn'] = $ip['eip_vpn'] ?? [];
$user['eip_relay'] = $ip['eip_relay'] ?? [];
$user['eip_starlink'] = $ip['eip_starlink'] ?? [];
$user['eip_total_visit'] = $ip['eip_total_visit'] ?? [];
$user['eip_blocklist'] = $ip['eip_blocklist'] ?? [];
$user['eip_shared'] = $ip['eip_shared'] ?? [];
//$user['eip_domains'] = $ip['eip_domains'] ?? [];
$user['eip_country_id'] = $ip['eip_country_id'] ?? [];
$user['eip_fraud_detected'] = $ip['eip_fraud_detected'] ?? [];
$user['eip_alert_list'] = $ip['eip_alert_list'] ?? [];
$user['eip_domains_count_len'] = $ip['eip_domains_count_len'] ?? [];
$user['eup_device'] = $device['eup_device'] ?? [];
$user['eup_device_id'] = $device['eup_device_id'] ?? [];
$user['eup_browser_name'] = $device['eup_browser_name'] ?? [];
$user['eup_browser_version'] = $device['eup_browser_version'] ?? [];
$user['eup_os_name'] = $device['eup_os_name'] ?? [];
$user['eup_lang'] = $device['eup_lang'] ?? [];
$user['eup_ua'] = $device['eup_ua'] ?? [];
// $user['eup_lastseen'] = $device['eup_lastseen'] ?? [];
// $user['eup_created'] = $device['eup_created'] ?? [];
$user['ee_email'] = $email['ee_email'] ?? [];
$user['ee_earliest_breach'] = $email['ee_earliest_breach'] ?? [];
$user['ep_phone_number'] = $phone['ep_phone_number'] ?? [];
$user['ep_shared'] = $phone['ep_shared'] ?? [];
$user['ep_type'] = $phone['ep_type'] ?? [];
$user['event_ip'] = $events['event_ip'] ?? [];
$user['event_url_string'] = $events['event_url_string'] ?? [];
$user['event_empty_referer'] = $events['event_empty_referer'] ?? [];
$user['event_device'] = $events['event_device'] ?? [];
$user['event_type'] = $events['event_type'] ?? [];
$user['event_http_method'] = $events['event_http_method'] ?? [];
$user['event_http_code'] = $events['event_http_code'] ?? [];
$user['event_device_created'] = $events['event_device_created'] ?? [];
$user['event_device_lastseen'] = $events['event_device_lastseen'] ?? [];
$user['event_session_multiple_country'] = $session[0]['event_session_multiple_country'] ?? false;
$user['event_session_multiple_ip'] = $session[0]['event_session_multiple_ip'] ?? false;
$user['event_session_multiple_device'] = $session[0]['event_session_multiple_device'] ?? false;
$user['event_session_night_time'] = $session[0]['event_session_night_time'] ?? false;
//Extra params for rules
$user = $this->extendParams($user);
$userDetails[$userId] = $this->extendEventParams($user);
}
return $userDetails;
}
private function extendParams(array $record): array {
//$record['timezone']
$localPartLen = strlen($record['le_local_part']);
$domainPartLen = strlen($record['le_domain_part']);
$fullName = $this->getUserFullName($record);
$record['le_local_part_len'] = $localPartLen;
$record['ea_fullname_has_numbers'] = preg_match('~[0-9]+~', $fullName) > 0;
$record['ea_fullname_has_spaces_hyphens'] = preg_match('~[\-\s]~', $fullName) > 0;
$record['ea_days_since_account_creation'] = $this->getDaysSinceAccountCreation($record);
$record['ea_days_since_last_visit'] = $this->getDaysSinceLastVisit($record);
//$record['le_has_no_profiles'] = $record['le_profiles'] === 0;
$record['le_has_no_data_breaches'] = $record['le_data_breach'] === false;
$record['le_has_suspicious_str'] = $this->checkEmailForSuspiciousString($record);
$record['le_has_numeric_only_local_part'] = preg_match('/^[0-9]+$/', $record['le_local_part']) > 0;
$record['le_email_has_consec_s_chars'] = preg_match('/[^a-zA-Z0-9]{2,}/', $record['le_local_part']) > 0;
$record['le_email_has_consec_nums'] = preg_match('/\d{2}/', $record['le_local_part']) > 0;
$record['le_email_has_no_digits'] = !preg_match('/\d/', $record['le_local_part']);
$record['le_email_has_vowels'] = preg_match('/[aeoui]/i', $record['le_local_part']) > 0;
$record['le_email_has_consonants'] = preg_match('/[bcdfghjklmnpqrstvwxyz]/i', $record['le_local_part']) > 0;
$record['le_with_long_local_part_length'] = $localPartLen > \Utils\Constants::get('RULE_EMAIL_MAXIMUM_LOCAL_PART_LENGTH');
$record['le_with_long_domain_length'] = $domainPartLen > \Utils\Constants::get('RULE_EMAIL_MAXIMUM_DOMAIN_LENGTH');
$record['le_email_in_blockemails'] = $record['le_blockemails'] ?? false;
$record['le_is_invalid'] = $record['le_exists'] && filter_var($record['le_email'], FILTER_VALIDATE_EMAIL) === false;
$record['le_appears_on_alert_list'] = $record['le_alert_list'] ?? false;
$record['ld_is_disposable'] = $record['ld_disposable_domains'] ?? false;
$record['ld_days_since_domain_creation'] = $this->getDaysSinceDomainCreation($record);
$record['ld_domain_free_email_provider'] = $record['ld_free_email_provider'] ?? false;
$record['ld_from_blockdomains'] = $record['ld_blockdomains'] ?? false;
$record['ld_domain_without_mx_record'] = $record['ld_mx_record'] === false;
$record['ld_website_is_disabled'] = $record['ld_disabled'] ?? false;
$record['ld_tranco_rank'] = $record['ld_tranco_rank'] ?? -1;
$record['lp_invalid_phone'] = $record['lp_invalid'] === true;
$record['ep_shared_phone'] = (bool) count(array_filter($record['ep_shared'], static function ($item) {
return $item !== null && $item > 1;
}));
$daysSinceBreaches = array_map(function ($item) {
return $this->getDaysTillToday($item);
}, $record['ee_earliest_breach']);
$record['ee_days_since_first_breach'] = count($daysSinceBreaches) ? max($daysSinceBreaches) : -1;
$onlyNonResidentialParams = !(bool) count(array_filter(array_merge(
$record['eip_fraud_detected'],
$record['eip_blocklist'],
$record['eip_tor'],
$record['eip_starlink'],
$record['eip_relay'],
$record['eip_vpn'],
$record['eip_data_center'],
), static function ($value): bool {
return $value === true;
}));
$record['eip_only_residential'] = $onlyNonResidentialParams && !in_array(0, $record['eip_country_id']);
$record['eip_has_fraud'] = in_array(true, $record['eip_fraud_detected']);
$record['eip_unique_cidrs'] = count(array_unique($record['eip_cidr']));
$record['lp_fraud_detected'] = $record['lp_fraud_detected'] ?? false;
$record['le_fraud_detected'] = $record['le_fraud_detected'] ?? false;
$record['eup_has_rare_browser'] = (bool) count(array_diff($record['eup_browser_name'], array_keys(\Utils\Constants::get('RULE_REGULAR_BROWSER_NAMES'))));
$record['eup_has_rare_os'] = (bool) count(array_diff($record['eup_os_name'], \Utils\Constants::get('RULE_REGULAR_OS_NAMES')));
$record['eup_device_count'] = count($record['eup_device']);
$record['eup_vulnerable_ua'] = false;
if (count($this->suspiciousWordsUserAgent)) {
foreach ($record['eup_ua'] as $url) {
foreach ($this->suspiciousWordsUserAgent as $sub) {
if (stripos($url, $sub) !== false) {
$record['eup_vulnerable_ua'] = true;
break 2;
}
}
}
}
return $record;
}
private function extendEventParams(array $record): array {
// Remove null values specifically
$eventTypeFiltered = $this->filterStringNum($record['event_type']);
$eventHttpCodeFiltered = $this->filterStringNum($record['event_http_code']);
$eventTypeCount = array_count_values($eventTypeFiltered);
//$accountLoginFailId = \Utils\Constants::get('ACCOUNT_LOGIN_FAIL_EVENT_TYPE_ID');
$accountEmailChangeId = \Utils\Constants::get('ACCOUNT_EMAIL_CHANGE_EVENT_TYPE_ID');
$accountPwdChangeId = \Utils\Constants::get('ACCOUNT_PASSWORD_CHANGE_EVENT_TYPE_ID');
//$record['event_failed_login_attempts'] = $eventTypeCount[$accountLoginFailId] ?? 0;
$record['event_email_changed'] = array_key_exists($accountEmailChangeId, $eventTypeCount);
$record['event_password_changed'] = array_key_exists($accountPwdChangeId, $eventTypeCount);
$record['event_http_method_head'] = in_array(\Utils\Constants::get('EVENT_REQUEST_TYPE_HEAD'), $record['event_http_method']);
$record['event_empty_referer'] = in_array(true, $record['event_empty_referer'], true);
$clientErrors = 0;
$serverErrors = 0;
$successEvents = 0;
foreach ($eventHttpCodeFiltered as $code) {
if (is_int($code) && $code >= 400 && $code < 500) {
++$clientErrors;
} elseif (is_int($code) && $code >= 500 && $code < 600) {
++$serverErrors;
} elseif (is_int($code) && $code >= 200 && $code < 300) {
++$successEvents;
}
}
$record['event_multiple_5xx_http'] = $serverErrors;
$record['event_multiple_4xx_http'] = $clientErrors;
$record['event_2xx_http'] = (bool) $successEvents;
$record['event_vulnerable_url'] = false;
if (count($this->suspiciousWordsUrl)) {
foreach ($record['event_url_string'] as $url) {
foreach ($this->suspiciousWordsUrl as $sub) {
if (stripos($url, $sub) !== false) {
$record['event_vulnerable_url'] = true;
break 2;
}
}
}
}
return $record;
}
private function getDaysSinceDomainCreation(array $params): int {
$dt1 = date('Y-m-d');
$dt2 = $params['ld_creation_date'];
return $this->getDaysDiff($dt1, $dt2);
}
private function getDaysSinceAccountCreation(array $params): int {
$dt1 = date('Y-m-d');
$dt2 = $params['ea_created'] ?? null;
return $this->getDaysDiff($dt1, $dt2);
}
private function getDaysSinceLastVisit(array $params): int {
$dt1 = date('Y-m-d');
$dt2 = $params['ea_lastseen'] ?? null;
return $this->getDaysDiff($dt1, $dt2);
}
private function getDaysTillToday(?string $dt2): int {
$diff = -1;
if ($dt2 !== null) {
$dt1 = date('Y-m-d');
$dt1 = new \DateTime($dt1);
$dt2 = new \DateTime($dt2);
$diff = $dt1->diff($dt2)->format('%a');
}
return $diff;
}
private function getDaysDiff(?string $dt1, ?string $dt2): int {
$diff = -1;
if ($dt2) {
$dt1 = new \DateTime($dt1);
$dt2 = new \DateTime($dt2);
$diff = $dt1->diff($dt2)->format('%a');
}
return $diff;
}
private function getUserFullName(array $record): string {
$name = [];
$fName = $record['ea_firstname'] ?? '';
if ($fName) {
$name[] = $fName;
}
$lName = $record['ea_lastname'] ?? '';
if ($lName) {
$name[] = $lName;
}
return trim(join(' ', $name));
}
private function checkEmailForSuspiciousString(array $record): bool {
foreach ($this->suspiciousWordsEmail as $sub) {
if (stripos($record['le_email'], $sub) !== false) {
return true;
}
}
return false;
}
private function filterStringNum(array $record): array {
return array_filter($record, static function ($value): bool {
return is_string($value) || is_int($value);
});
}
}

View File

@@ -0,0 +1,70 @@
<?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 Models\Context;
class Device extends Base {
public function getContext(array $accountIds, int $apiKey): array {
$record = $this->getDetails($accountIds, $apiKey);
$recordByAccount = $this->groupRecordsByAccount($record);
foreach ($recordByAccount as $key => $value) {
$recordByAccount[$key] = [
'eup_device' => array_column($value, 'eup_device'),
'eup_device_id' => array_column($value, 'eup_device_id'),
'eup_browser_name' => array_column($value, 'eup_browser_name'),
'eup_browser_version' => array_column($value, 'eup_browser_version'),
'eup_os_name' => array_column($value, 'eup_os_name'),
'eup_lang' => array_column($value, 'eup_lang'),
'eup_ua' => array_column($value, 'eup_ua'),
// 'eup_lastseen' => array_column($value, 'eup_lastseen'),
// 'eup_created' => array_column($value, 'eup_created'),
];
}
return $recordByAccount;
}
protected function getDetails(array $accountIds, int $apiKey): array {
[$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey);
$query = (
"SELECT
event_device.account_id AS accountid,
event_device.id AS eup_device_id,
event_ua_parsed.device AS eup_device,
event_ua_parsed.browser_name AS eup_browser_name,
event_ua_parsed.browser_version AS eup_browser_version,
event_ua_parsed.os_name AS eup_os_name,
event_ua_parsed.ua AS eup_ua,
-- event_device.lastseen AS eup_lastseen,
-- event_device.created AS eup_created,
event_device.lang AS eup_lang
FROM
event_device
INNER JOIN event_ua_parsed
ON(event_device.user_agent=event_ua_parsed.id)
WHERE
event_device.key = :api_key
AND event_ua_parsed.checked = true
AND event_device.account_id IN ({$placeHolders})"
);
return $this->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,65 @@
<?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 Models\Context;
class Domain extends Base {
public function getContext(array $accountIds, int $apiKey): array {
$records = $this->getDetails($accountIds, $apiKey);
$recordsByAccount = $this->groupRecordsByAccount($records);
foreach ($recordsByAccount as $key => $value) {
$recordsByAccount[$key] = [
'ed_domain' => $this->getUniqueArray(array_column($value, 'ed_domain')),
'ed_blockdomains' => $this->getUniqueArray(array_column($value, 'ed_blockdomains')),
'ed_disposable_domains' => $this->getUniqueArray(array_column($value, 'ed_disposable_domains')),
'ed_free_email_provider' => $this->getUniqueArray(array_column($value, 'ed_free_email_provider')),
'ed_creation_date' => $this->getUniqueArray(array_column($value, 'ed_creation_date')),
'ed_disabled' => $this->getUniqueArray(array_column($value, 'ed_disabled')),
'ed_mx_record' => $this->getUniqueArray(array_column($value, 'ed_mx_record')),
];
}
return $recordsByAccount;
}
protected function getDetails(array $accountIds, int $apiKey): array {
[$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey);
$query = (
"SELECT
event_email.account_id AS accountid,
event_domain.domain AS ed_domain,
event_domain.blockdomains AS ed_blockdomains,
event_domain.disposable_domains AS ed_disposable_domains,
event_domain.free_email_provider AS ed_free_email_provider,
event_domain.creation_date AS ed_creation_date,
event_domain.disabled AS ed_disabled,
event_domain.mx_record AS ed_mx_record
FROM
event_domain
INNER JOIN event_email
ON event_domain.id = event_email.domain
WHERE
event_email.key = :api_key
AND event_email.account_id IN ({$placeHolders})"
);
return $this->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,52 @@
<?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 Models\Context;
class Email extends Base {
public function getContext(array $accountIds, int $apiKey): array {
$records = $this->getDetails($accountIds, $apiKey);
$recordsByAccount = $this->groupRecordsByAccount($records);
foreach ($recordsByAccount as $key => $value) {
$recordsByAccount[$key] = [
'ee_email' => $this->getUniqueArray(array_column($value, 'ee_email')),
'ee_earliest_breach' => $this->getUniqueArray(array_column($value, 'ee_earliest_breach')),
];
}
return $recordsByAccount;
}
protected function getDetails(array $accountIds, int $apiKey): array {
[$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey);
$query = (
"SELECT
event_email.account_id AS accountid,
event_email.email AS ee_email,
event_email.earliest_breach AS ee_earliest_breach
FROM
event_email
WHERE
event_email.key = :api_key
AND event_email.checked = 'True'
AND event_email.account_id IN ({$placeHolders})"
);
return $this->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,84 @@
<?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 Models\Context;
class Event extends Base {
public function getContext(array $accountIds, int $apiKey): array {
$records = $this->getDetails($accountIds, $apiKey);
$recordsByAccount = $this->groupRecordsByAccount($records);
foreach ($recordsByAccount as $key => $value) {
$recordsByAccount[$key] = [
'event_ip' => array_column($value, 'event_ip'),
'event_url_string' => array_column($value, 'event_url_string'),
'event_empty_referer' => array_column($value, 'event_empty_referer'),
'event_device' => array_column($value, 'event_device'),
'event_type' => array_column($value, 'event_type'),
'event_http_code' => array_column($value, 'event_http_code'),
'event_device_created' => array_column($value, 'event_device_created'),
'event_device_lastseen' => array_column($value, 'event_device_lastseen'),
'event_http_method' => array_column($value, 'event_http_method'),
];
}
return $recordsByAccount;
}
protected function getDetails(array $accountIds, int $apiKey): array {
[$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey);
$contextLimit = \Utils\Constants::get('RULE_EVENT_CONTEXT_LIMIT');
$query = (
"WITH ranked_events AS (
SELECT
event.account AS accountid,
event.id AS event_id,
event.ip AS event_ip,
event_url.url AS event_url_string,
event_referer.referer AS event_referer_string,
event.device AS event_device,
event.time AS event_time,
event.type AS event_type,
event.http_code AS event_http_code,
event.http_method AS event_http_method,
ROW_NUMBER() OVER (PARTITION BY event.account ORDER BY event.time DESC) AS rn
FROM event
LEFT JOIN event_url ON event_url.id = event.url
LEFT JOIN event_referer ON event_referer.id = event.referer
WHERE event.key = :api_key
AND event.account IN ({$placeHolders})
)
SELECT
accountid,
event_ip,
event_url_string,
(event_referer_string IS NULL OR event_referer_string = '') AS event_empty_referer,
event_device,
ed.created AS event_device_created,
ed.lastseen AS event_device_lastseen,
event_type,
event_http_code,
event_http_method
FROM ranked_events
LEFT JOIN event_device AS ed
ON ranked_events.event_device = ed.id
WHERE rn <= {$contextLimit}
ORDER BY event_time DESC;"
);
return $this->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,91 @@
<?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 Models\Context;
class Ip extends Base {
public function getContext(array $accountIds, int $apiKey): array {
$records = $this->getDetails($accountIds, $apiKey);
$recordsByAccount = $this->groupRecordsByAccount($records);
foreach ($recordsByAccount as $key => $value) {
$recordsByAccount[$key] = [
'eip_ip_id' => array_column($value, 'eip_ip_id'),
'eip_ip' => array_column($value, 'eip_ip'),
'eip_cidr' => array_column($value, 'eip_cidr'),
'eip_data_center' => array_column($value, 'eip_data_center'),
'eip_tor' => array_column($value, 'eip_tor'),
'eip_vpn' => array_column($value, 'eip_vpn'),
'eip_relay' => array_column($value, 'eip_relay'),
'eip_starlink' => array_column($value, 'eip_starlink'),
'eip_total_visit' => array_column($value, 'eip_total_visit'),
'eip_blocklist' => array_column($value, 'eip_blocklist'),
'eip_shared' => array_column($value, 'eip_shared'),
//'eip_domains' => $this->getUniqueArray(array_column($value, 'eip_domains')),
'eip_domains_count_len' => array_column($value, 'eip_domains_count_len'),
'eip_country_id' => array_column($value, 'eip_country_id'),
'eip_fraud_detected' => array_column($value, 'eip_fraud_detected'),
'eip_alert_list' => array_column($value, 'eip_alert_list'),
];
}
return $recordsByAccount;
}
protected function getDetails(array $accountIds, int $apiKey): array {
[$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey);
$query = (
"SELECT DISTINCT
event.account AS accountid,
event_ip.id AS eip_ip_id,
event_ip.ip AS eip_ip,
event_ip.cidr::text AS eip_cidr,
event_ip.country AS eip_country_id,
event_ip.data_center AS eip_data_center,
event_ip.tor AS eip_tor,
event_ip.vpn AS eip_vpn,
event_ip.relay AS eip_relay,
event_ip.starlink AS eip_starlink,
event_ip.total_visit AS eip_total_visit,
event_ip.blocklist AS eip_blocklist,
event_ip.shared AS eip_shared,
-- event_ip.domains_count AS eip_domains,
json_array_length(event_ip.domains_count::json) AS eip_domains_count_len,
event_ip.fraud_detected AS eip_fraud_detected,
event_ip.alert_list AS eip_alert_list
FROM
event_ip
INNER JOIN event
ON (event_ip.id = event.ip)
WHERE
event_ip.key = :api_key
AND event_ip.checked = 'True'
AND event.account IN ({$placeHolders})
-- ORDER BY event_ip.id DESC"
);
if (count($accountIds) === 1) {
$query .= ' LIMIT 100 OFFSET 0';
}
return $this->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,79 @@
<?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 Models\Context;
class Phone extends Base {
public function getContext(array $accountIds, int $apiKey): array {
$records = $this->getDetails($accountIds, $apiKey);
$recordsByAccount = $this->groupRecordsByAccount($records);
foreach ($recordsByAccount as $key => $value) {
$recordsByAccount[$key] = [
//'ep_calling_country_code' => $this->getUniqueArray(array_column($value, 'ep_calling_country_code')),
//'ep_carrier_name' => $this->getUniqueArray(array_column($value, 'ep_carrier_name')),
//'ep_checked' => $this->getUniqueArray(array_column($value, 'ep_checked')),
//'ep_country_code' => $this->getUniqueArray(array_column($value, 'ep_country_code')),
//'ep_created' => $this->getUniqueArray(array_column($value, 'ep_created')),
//'ep_lastseen' => $this->getUniqueArray(array_column($value, 'ep_lastseen')),
//'ep_mobile_country_code' => $this->getUniqueArray(array_column($value, 'ep_mobile_country_code')),
//'ep_mobile_network_code' => $this->getUniqueArray(array_column($value, 'ep_mobile_network_code')),
//'ep_national_format' => $this->getUniqueArray(array_column($value, 'ep_national_format')),
'ep_phone_number' => $this->getUniqueArray(array_column($value, 'ep_phone_number')),
'ep_shared' => $this->getUniqueArray(array_column($value, 'ep_shared')),
'ep_type' => $this->getUniqueArray(array_column($value, 'ep_type')),
//'ep_invalid' => $this->getUniqueArray(array_column($value, 'ep_invalid')),
//'ep_validation_errors' => $this->getUniqueArray(array_column($value, 'ep_validation_errors')),
//'ep_alert_list' => $this->getUniqueArray(array_column($value, 'ep_alert_list')),
];
}
return $recordsByAccount;
}
protected function getDetails(array $accountIds, int $apiKey): array {
[$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey);
$query = (
"SELECT
event_phone.account_id AS accountid,
-- event_phone.calling_country_code AS ep_calling_country_code,
-- event_phone.carrier_name AS ep_carrier_name,
-- event_phone.checked AS ep_checked,
-- event_phone.country_code AS ep_country_code,
-- event_phone.created AS ep_created,
-- event_phone.lastseen AS ep_lastseen,
-- event_phone.mobile_country_code AS ep_mobile_country_code,
-- event_phone.mobile_network_code AS ep_mobile_network_code,
-- event_phone.national_format AS ep_national_format,
event_phone.phone_number AS ep_phone_number,
event_phone.shared AS ep_shared,
event_phone.type AS ep_type
-- event_phone.invalid AS ep_invalid,
-- event_phone.validation_errors AS ep_validation_errors,
-- event_phone.alert_list AS ep_alert_list
FROM
event_phone
WHERE
event_phone.key = :api_key
AND event_phone.account_id IN ({$placeHolders})"
);
return $this->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,71 @@
<?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 Models\Context;
class Session extends Base {
public function getContext(array $accountIds, int $apiKey, int $timezoneOffset = 0): array {
$records = $this->getDetails($accountIds, $apiKey, $timezoneOffset);
// one record per account
$recordsByAccount = $this->groupRecordsByAccount($records);
return $recordsByAccount;
}
protected function getDetails(array $accountIds, int $apiKey, int $timezoneOffset = 0): array {
[$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey);
$params[':night_start'] = gmdate('H:i:s', \Utils\Constants::get('NIGHT_RANGE_SECONDS_START') - $timezoneOffset);
$params[':night_end'] = gmdate('H:i:s', \Utils\Constants::get('NIGHT_RANGE_SECONDS_END') - $timezoneOffset);
// boolean logic for defining time ranges overlap
$query = (
"SELECT
event_session.account_id AS accountid,
BOOL_OR(event_session.total_country > 1) AS event_session_multiple_country,
BOOL_OR(event_session.total_ip > 1) AS event_session_multiple_ip,
BOOL_OR(event_session.total_device > 1) AS event_session_multiple_device,
BOOL_OR(
(event_session.lastseen - event_session.created) > INTERVAL '1 day' OR
(
CASE WHEN :night_start::time < :night_end::time
THEN
(event_session.lastseen::time >= :night_start::time AND event_session.lastseen::time <= :night_end::time) OR
(event_session.created::time >= :night_start::time AND event_session.created::time <= :night_end::time) OR
(
CASE WHEN event_session.lastseen::time > event_session.created::time
THEN
event_session.total_visit > 1 AND :night_start::time >= event_session.created::time AND :night_start::time <= event_session.lastseen::time
ELSE
event_session.total_visit > 1 AND (:night_start::time >= event_session.created::time OR :night_start::time <= event_session.lastseen::time)
END
)
ELSE
event_session.lastseen::time >= :night_start::time OR event_session.lastseen::time <= :night_end::time OR
event_session.created::time >= :night_start::time OR event_session.created::time <= :night_end::time OR
event_session.lastseen::time < event_session.created::time
END
)) AS event_session_night_time
FROM
event_session
WHERE
event_session.key = :api_key AND
event_session.account_id IN ({$placeHolders})
GROUP BY event_session.account_id"
);
return $this->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,118 @@
<?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 Models\Context;
class User extends Base {
use \Traits\Enrichment\Emails;
public function getContext(array $accountIds, int $apiKey): array {
$results = $this->getDetails($accountIds, $apiKey);
$this->calculateEmailReputationForContext($results);
$recordsByAccount = [];
foreach ($results as $item) {
$recordsByAccount[$item['ea_id']] = $item;
}
return $recordsByAccount;
}
protected function getDetails(array $accountIds, int $apiKey): array {
[$params, $placeHolders] = $this->getRequestParams($accountIds, $apiKey);
$query = (
"SELECT
event_account.id AS ea_id,
event_account.userid AS ea_userid,
event_account.created AS ea_created,
event_account.lastseen AS ea_lastseen,
event_account.total_visit AS ea_total_visit,
event_account.total_country AS ea_total_country,
event_account.total_ip AS ea_total_ip,
event_account.total_device AS ea_total_device,
event_account.firstname AS ea_firstname,
event_account.lastname AS ea_lastname,
event_email.email AS ee_email,
event_email.blockemails AS ee_blockemails,
event_email.data_breach AS ee_data_breach,
-- event_email.profiles AS ee_profiles,
event_email.checked AS ee_checked,
event_domain.discovery_date AS ed_discovery_date,
event_domain.blockdomains AS ed_blockdomains,
event_domain.disposable_domains AS ed_disposable_domains,
-- event_domain.total_account AS ed_total_account,
event_domain.free_email_provider AS ed_free_provider,
event_domain.tranco_rank AS ed_tranco_rank,
event_domain.creation_date AS ed_creation_date,
event_domain.expiration_date AS ed_expiration_date,
event_domain.return_code AS ed_return_code,
event_domain.closest_snapshot AS ed_closest_snapshot,
event_domain.mx_record AS ed_mx_record,
lastemail_record.email AS le_email,
lastemail_record.blockemails AS le_blockemails,
lastemail_record.data_breach AS le_data_breach,
-- lastemail_record.profiles AS le_profiles,
lastemail_record.checked AS le_checked,
lastemail_record.fraud_detected AS le_fraud_detected,
lastemail_record.alert_list AS le_alert_list,
lastdomain_record.disposable_domains AS ld_disposable_domains,
lastdomain_record.free_email_provider AS ld_free_email_provider,
lastdomain_record.blockdomains AS ld_blockdomains,
lastdomain_record.mx_record AS ld_mx_record,
lastdomain_record.disabled AS ld_disabled,
lastdomain_record.creation_date AS ld_creation_date,
lastdomain_record.tranco_rank AS ld_tranco_rank,
lastphone_record.phone_number AS lp_phone_number,
lastphone_record.country_code AS lp_country_code,
lastphone_record.invalid AS lp_invalid,
lastphone_record.fraud_detected AS lp_fraud_detected,
lastphone_record.alert_list AS lp_alert_list
FROM
event_account
LEFT JOIN event_phone
ON (event_account.id = event_phone.account_id)
LEFT JOIN event_email
ON event_account.id = event_email.account_id
LEFT JOIN event_domain
ON event_email.domain = event_domain.id
LEFT JOIN event_email AS lastemail_record
ON event_account.lastemail = lastemail_record.id
LEFT JOIN event_phone AS lastphone_record
ON event_account.lastphone = lastphone_record.id
LEFT JOIN event_domain AS lastdomain_record
ON lastemail_record.domain = lastdomain_record.id
WHERE
event_account.key = :api_key
AND event_account.id IN ({$placeHolders})"
);
return $this->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,292 @@
<?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 Models;
class Country extends \Models\BaseSql implements \Interfaces\ApiKeyAccessAuthorizationInterface {
protected $DB_TABLE_NAME = 'event_country';
public function getCountryById(int $countryId, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':country_id' => $countryId,
];
$query = (
'SELECT
countries.iso,
countries.value
FROM
event_country
INNER JOIN countries
ON (event_country.country = countries.id)
WHERE
event_country.key = :api_key
AND event_country.country = :country_id'
);
$results = $this->execQuery($query, $params);
return $results[0] ?? [];
}
public function getCountryIdByIso(string $countryIso): int {
$params = [
':country_iso' => $countryIso,
];
$query = (
'SELECT
countries.id
FROM
countries
WHERE
countries.iso = :country_iso'
);
$results = $this->execQuery($query, $params);
return $results[0]['id'] ?? 0;
}
public function checkAccess(int $subjectId, int $apiKey): bool {
$params = [
':api_key' => $apiKey,
':country_id' => $subjectId,
];
$query = (
'SELECT
event_country.country
FROM
event_country
WHERE
event_country.key = :api_key
AND event_country.country = :country_id'
);
$results = $this->execQuery($query, $params);
return count($results) > 0;
}
public function insertRecord(array $data, int $apiKey): int {
$params = [
':key' => $apiKey,
':country' => $data['id'],
':lastseen' => $data['lastseen'],
':updated' => $data['lastseen'],
];
$query = (
'INSERT INTO event_country (
key, country, lastseen, updated
) VALUES (
:key, :country, :lastseen, :updated
) ON CONFLICT (country, key) DO UPDATE SET
lastseen = EXCLUDED.lastseen
RETURNING id'
);
$results = $this->execQuery($query, $params);
return $results[0]['id'];
}
public function getTimeFrameTotal(array $ids, string $startDate, string $endDate, int $apiKey): array {
[$params, $flatIds] = $this->getArrayPlaceholders($ids);
$params[':key'] = $apiKey;
$params[':start_date'] = $startDate;
$params[':end_date'] = $endDate;
$query = (
"SELECT
event_ip.country AS id,
COUNT(*) AS cnt
FROM event
INNER JOIN event_ip
ON event.ip = event_ip.id
WHERE
event_ip.country IN ({$flatIds}) AND
event.key = :key AND
event.time > :start_date AND
event.time < :end_date
GROUP BY event_ip.country"
);
$totalVisit = $this->execQuery($query, $params);
$query = (
"SELECT
event_ip.country AS id,
COUNT(DISTINCT(event.account)) AS cnt
FROM event
INNER JOIN event_ip
ON event.ip = event_ip.id
WHERE
event_ip.country IN ({$flatIds}) AND
event.key = :key AND
event.time > :start_date AND
event.time < :end_date
GROUP BY event_ip.country"
);
$totalAccount = $this->execQuery($query, $params);
$query = (
"SELECT
event_ip.country AS id,
COUNT(*) AS cnt
FROM event_ip
WHERE
event_ip.country IN ({$flatIds}) AND
event_ip.key = :key AND
event_ip.lastseen > :start_date AND
event_ip.lastseen < :end_date
GROUP BY event_ip.country"
);
$totalIp = $this->execQuery($query, $params);
$result = [];
foreach ($ids as $id) {
$result[$id] = ['total_visit' => 0, 'total_account' => 0, 'total_ip' => 0];
}
foreach ($totalVisit as $rec) {
$result[$rec['id']]['total_visit'] = $rec['cnt'];
}
foreach ($totalAccount as $rec) {
$result[$rec['id']]['total_account'] = $rec['cnt'];
}
foreach ($totalIp as $rec) {
$result[$rec['id']]['total_ip'] = $rec['cnt'];
}
return $result;
}
public function updateTotalsByEntityIds(array $ids, int $apiKey, bool $force = false): void {
if (!count($ids)) {
return;
}
[$params, $flatIds] = $this->getArrayPlaceholders($ids);
$params[':key'] = $apiKey;
$extraClause = $force ? '' : ' AND event_country.lastseen >= event_country.updated';
$query = (
"UPDATE event_country
SET
total_visit = COALESCE(sub.total_visit, 0),
total_account = COALESCE(sub.total_account, 0),
total_ip = COALESCE(sub.total_ip, 0),
updated = date_trunc('milliseconds', now())
FROM (
SELECT
event_ip.country,
COUNT(*) AS total_visit,
COUNT(DISTINCT event.account) AS total_account,
COUNT(DISTINCT event.ip) AS total_ip
FROM event
JOIN event_ip ON event.ip = event_ip.id
WHERE
event_ip.country IN ($flatIds) AND
event.key = :key
GROUP BY event_ip.country
) AS sub
RIGHT JOIN countries sub_country ON sub.country = sub_country.id
WHERE
event_country.country = sub_country.id AND
event_country.country IN ($flatIds) AND
event_country.key = :key
$extraClause"
);
$this->execQuery($query, $params);
}
public function updateAllTotals(int $apiKey): int {
$params = [
':key' => $apiKey,
];
$query = (
'UPDATE event_country
SET
total_visit = COALESCE(sub.total_visit, 0),
total_account = COALESCE(sub.total_account, 0),
total_ip = COALESCE(sub.total_ip, 0),
updated = date_trunc(\'milliseconds\', now())
FROM (
SELECT
event_ip.country,
COUNT(*) AS total_visit,
COUNT(DISTINCT event.account) AS total_account,
COUNT(DISTINCT event.ip) AS total_ip
FROM event
JOIN event_ip ON event.ip = event_ip.id AND event.key = event_ip.key
WHERE event.key = :key
GROUP BY event_ip.country
) AS sub
RIGHT JOIN countries sub_country ON sub.country = sub_country.id
WHERE
event_country.key = :key AND
event_country.country = sub_country.id AND
event_country.lastseen >= event_country.updated'
);
return $this->execQuery($query, $params);
}
public function refreshTotals(array $res, int $apiKey): array {
[$params, $flatIds] = $this->getArrayPlaceholders(array_column($res, 'id'));
$params[':key'] = $apiKey;
$query = (
"SELECT
country AS id,
total_ip,
total_visit,
total_account
FROM event_country
WHERE country IN ({$flatIds}) AND key = :key"
);
$result = $this->execQuery($query, $params);
$indexedResult = [];
foreach ($result as $item) {
$indexedResult[$item['id']] = $item;
}
foreach ($res as $idx => $item) {
$item['total_ip'] = $indexedResult[$item['id']]['total_ip'];
$item['total_visit'] = $indexedResult[$item['id']]['total_visit'];
$item['total_account'] = $indexedResult[$item['id']]['total_account'];
$res[$idx] = $item;
}
return $res;
}
}

View File

@@ -0,0 +1,159 @@
<?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 Models;
class Dashboard extends \Models\BaseSql {
protected $DB_TABLE_NAME = 'event_account';
public function getTotalBlockedUsers(?array $dateRange, int $apiKey): int {
$query = (
'SELECT
COUNT(event_account.id)
FROM
event_account
WHERE
event_account.key = :api_key AND
event_account.fraud IS TRUE'
);
$field = 'event_account.latest_decision';
return $this->getTotal($query, $field, $dateRange, $apiKey);
}
public function getTotalUsersForReview(?array $dateRange, int $apiKey): int {
$query = (
'SELECT
COUNT(event_account.id)
FROM
event_account
WHERE
event_account.key = :api_key AND
event_account.fraud IS NULL AND
event_account.added_to_review IS NOT NULL'
);
$field = 'event_account.added_to_review';
return $this->getTotal($query, $field, $dateRange, $apiKey);
}
public function getTotalEvents(?array $dateRange, int $apiKey): int {
$query = (
'SELECT
COUNT(*)
FROM
event
WHERE
event.key = :api_key'
);
$field = 'event.time';
return $this->getTotal($query, $field, $dateRange, $apiKey);
}
public function getTotalResources(?array $dateRange, int $apiKey): int {
$query = (
'SELECT
COUNT(*)
FROM
event_url
WHERE
event_url.key = :api_key'
);
$field = 'event_url.lastseen';
return $this->getTotal($query, $field, $dateRange, $apiKey);
}
public function getTotalCountries(?array $dateRange, int $apiKey): int {
$query = (
'SELECT
COUNT(event_country.id)
FROM
event_country
WHERE
event_country.key = :api_key'
);
$field = 'event_country.lastseen';
return $this->getTotal($query, $field, $dateRange, $apiKey);
}
public function getTotalIps(?array $dateRange, int $apiKey): int {
$query = (
'SELECT
COUNT (*)
FROM
event_ip
WHERE
event_ip.key = :api_key'
);
$field = 'event_ip.lastseen';
return $this->getTotal($query, $field, $dateRange, $apiKey);
}
public function getTotalUsers(?array $dateRange, int $apiKey): int {
$query = (
'SELECT
COUNT (*)
FROM
event_account
WHERE
event_account.key = :api_key'
);
$field = 'event_account.lastseen';
return $this->getTotal($query, $field, $dateRange, $apiKey);
}
private function getTotal(string $query, string $dateField, ?array $dateRange, int $apiKey): int {
$params = [
':api_key' => $apiKey,
];
if ($dateRange) {
$params[':end_time'] = $dateRange['endDate'];
$params[':start_time'] = $dateRange['startDate'];
$query .= " AND {$dateField} >= :start_time AND {$dateField} <= :end_time";
}
$results = $this->execQuery($query, $params);
return $results[0]['count'] ?? 0;
}
}

View File

@@ -0,0 +1,187 @@
<?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 Models;
class Device extends \Models\BaseSql implements \Interfaces\ApiKeyAccessAuthorizationInterface {
protected $DB_TABLE_NAME = 'event_device';
public function checkAccess(int $subjectId, int $apiKey): bool {
$query = (
'SELECT
event_device.id
FROM
event_device
WHERE
event_device.key = :api_key
AND event_device.id = :device_id'
);
$params = [
':api_key' => $apiKey,
':device_id' => $subjectId,
];
$results = $this->execQuery($query, $params);
return count($results) > 0;
}
public function getFullDeviceInfoById(int $deviceId, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':device_id' => $deviceId,
];
$query = (
'SELECT
event_device.id,
event_device.lang,
event_device.created,
event_device.user_agent AS ua_id,
event_ua_parsed.device,
event_ua_parsed.browser_name,
event_ua_parsed.browser_version,
event_ua_parsed.os_name,
event_ua_parsed.os_version,
event_ua_parsed.ua,
event_ua_parsed.checked,
event_ua_parsed.modified
FROM
event_device
LEFT JOIN event_ua_parsed
ON (event_device.user_agent = event_ua_parsed.id)
WHERE
event_device.key = :api_key AND
event_device.id = :device_id'
);
$results = $this->execQuery($query, $params);
return $results[0] ?? [];
}
public function extractById(int $entityId, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':id' => $entityId,
];
$query = (
"SELECT
COALESCE(event_ua_parsed.ua, '') AS value
FROM
event_ua_parsed
WHERE
event_ua_parsed.key = :api_key AND
event_ua_parsed.id = :id
LIMIT 1"
);
$results = $this->execQuery($query, $params);
return $results[0] ?? [];
}
public function updateAllTotals(int $apiKey): int {
$params = [
':key' => $apiKey,
];
$query = (
'UPDATE event_device
SET
total_visit = COALESCE(sub.total_visit, 0),
updated = date_trunc(\'milliseconds\', now())
FROM (
SELECT
event.device,
COUNT(*) AS total_visit
FROM event
WHERE
event.key = :key
GROUP BY event.device
) AS sub
RIGHT JOIN event_device sub_device ON sub.device = sub_device.id
WHERE
event_device.id = sub_device.id AND
event_device.key = :key AND
event_device.lastseen >= event_device.updated'
);
return $this->execQuery($query, $params);
}
public function countNotChecked(int $apiKey): int {
$params = [
':key' => $apiKey,
];
$query = (
'SELECT
COUNT(*) AS count
FROM event_ua_parsed
WHERE
event_ua_parsed.key = :key AND
event_ua_parsed.checked IS FALSE'
);
$results = $this->execQuery($query, $params);
return $results[0]['count'] ?? 0;
}
public function notCheckedExists(int $apiKey): bool {
$params = [
':key' => $apiKey,
];
$query = (
'SELECT 1
FROM event_ua_parsed
WHERE
event_ua_parsed.key = :key AND
event_ua_parsed.checked IS FALSE
LIMIT 1'
);
$results = $this->execQuery($query, $params);
return (bool) count($results);
}
public function notCheckedForUserId(int $userId, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':user_id' => $userId,
];
$query = (
'SELECT DISTINCT
event_ua_parsed.id
FROM event_device
LEFT JOIN event_ua_parsed ON event_device.user_agent = event_ua_parsed.id
WHERE
event_device.account_id = :user_id AND
event_device.key = :api_key AND
event_ua_parsed.checked IS FALSE'
);
return array_column($this->execQuery($query, $params), 'id');
}
}

View File

@@ -0,0 +1,305 @@
<?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 Models;
class Domain extends \Models\BaseSql implements \Interfaces\ApiKeyAccessAuthorizationInterface {
protected $DB_TABLE_NAME = 'event_domain';
public function checkAccess(int $subjectId, int $apiKey): bool {
$params = [
':api_key' => $apiKey,
':domain_id' => $subjectId,
];
$query = (
'SELECT
event_domain.id
FROM
event_domain
WHERE
event_domain.key = :api_key
AND event_domain.id = :domain_id'
);
$results = $this->execQuery($query, $params);
return count($results) > 0;
}
public function getFullDomainInfoById(int $domainId, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':domain_id' => $domainId,
];
$query = (
'SELECT
event_domain.id,
event_domain.domain,
event_domain.total_account,
event_domain.lastseen,
event_domain.creation_date,
event_domain.expiration_date,
event_domain.disabled,
event_domain.disposable_domains,
event_domain.free_email_provider,
event_domain.tranco_rank,
event_domain.checked,
(
SELECT COUNT(*)
FROM event_email
WHERE
event_email.domain = event_domain.id AND
event_email.key = :api_key AND
event_email.fraud_detected IS TRUE
) AS fraud
FROM
event_domain
WHERE
event_domain.key = :api_key
AND event_domain.id = :domain_id
GROUP BY
event_domain.id'
);
$results = $this->execQuery($query, $params);
return $results[0] ?? [];
}
public function extractById(int $entityId, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':id' => $entityId,
];
$query = (
"SELECT
COALESCE(event_domain.domain, '') AS value
FROM
event_domain
WHERE
event_domain.key = :api_key
AND event_domain.id = :id
LIMIT 1"
);
$results = $this->execQuery($query, $params);
return $results[0] ?? [];
}
public function getTimeFrameTotal(array $ids, string $startDate, string $endDate, int $apiKey): array {
[$params, $flatIds] = $this->getArrayPlaceholders($ids);
$params[':key'] = $apiKey;
$params[':start_date'] = $startDate;
$params[':end_date'] = $endDate;
$query = (
"SELECT
event_email.domain AS id,
COUNT(DISTINCT(event.account)) AS cnt
FROM event
LEFT JOIN event_email
ON event.email = event_email.id
WHERE
event_email.domain IN ({$flatIds}) AND
event.key = :key AND
event_email.lastseen > :start_date AND
event_email.lastseen < :end_date
GROUP BY event_email.domain"
);
$totalAccount = $this->execQuery($query, $params);
$result = [];
foreach ($ids as $id) {
$result[$id] = ['total_account' => 0];
}
foreach ($totalAccount as $rec) {
$result[$rec['id']]['total_account'] = $rec['cnt'];
}
return $result;
}
public function updateTotalsByEntityIds(array $ids, int $apiKey, bool $force = false): void {
if (!count($ids)) {
return;
}
[$params, $flatIds] = $this->getArrayPlaceholders($ids);
$params[':key'] = $apiKey;
$extraClause = $force ? '' : ' AND event_domain.lastseen >= event_domain.updated';
$query = (
"UPDATE event_domain
SET
total_visit = COALESCE(sub.total_visit, 0),
total_account = COALESCE(sub.total_account, 0),
updated = date_trunc('milliseconds', now())
FROM (
SELECT
event_email.domain,
COUNT(*) AS total_visit,
COUNT(DISTINCT account) AS total_account
FROM event
LEFT JOIN event_email
ON event.email = event_email.id
WHERE
event_email.domain IN ($flatIds) AND
event.key = :key
GROUP BY event_email.domain
) AS sub
RIGHT JOIN event_domain sub_domain ON sub.domain = sub_domain.id
WHERE
event_domain.id = sub_domain.id AND
event_domain.id IN ($flatIds) AND
event_domain.key = :key
$extraClause"
);
$this->execQuery($query, $params);
}
public function updateAllTotals(int $apiKey): int {
$params = [
':key' => $apiKey,
];
$query = (
'UPDATE event_domain
SET
total_visit = COALESCE(sub.total_visit, 0),
total_account = COALESCE(sub.total_account, 0),
updated = date_trunc(\'milliseconds\', now())
FROM (
SELECT
event_email.domain,
COUNT(*) AS total_visit,
COUNT(DISTINCT account) AS total_account
FROM event
LEFT JOIN event_email ON event.email = event_email.id
WHERE
event.key = :key
GROUP BY event_email.domain
) AS sub
RIGHT JOIN event_domain sub_domain ON sub.domain = sub_domain.id
WHERE
event_domain.id = sub_domain.id AND
event_domain.key = :key AND
event_domain.lastseen >= event_domain.updated'
);
return $this->execQuery($query, $params);
}
public function refreshTotals(array $res, int $apiKey): array {
[$params, $flatIds] = $this->getArrayPlaceholders(array_column($res, 'id'));
$params[':key'] = $apiKey;
$query = (
"SELECT
id,
total_visit,
total_account
FROM event_domain
WHERE id IN ({$flatIds}) AND key = :key"
);
$result = $this->execQuery($query, $params);
$indexedResult = [];
foreach ($result as $item) {
$indexedResult[$item['id']] = $item;
}
foreach ($res as $idx => $item) {
$item['total_visit'] = $indexedResult[$item['id']]['total_visit'];
$item['total_account'] = $indexedResult[$item['id']]['total_account'];
$res[$idx] = $item;
}
return $res;
}
public function countNotChecked(int $apiKey): int {
$params = [
':key' => $apiKey,
];
$query = (
'SELECT
COUNT(*) AS count
FROM event_domain
WHERE
event_domain.key = :key AND
event_domain.checked IS FALSE'
);
$results = $this->execQuery($query, $params);
return $results[0]['count'] ?? 0;
}
public function notCheckedExists(int $apiKey): bool {
$params = [
':key' => $apiKey,
];
$query = (
'SELECT 1
FROM event_domain
WHERE
event_domain.key = :key AND
event_domain.checked IS FALSE
LIMIT 1'
);
$results = $this->execQuery($query, $params);
return (bool) count($results);
}
public function notCheckedForUserId(int $userId, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':user_id' => $userId,
];
$query = (
'SELECT DISTINCT
event_domain.id
FROM event_email
LEFT JOIN event_domain ON event_email.domain = event_domain.id
WHERE
event_email.account_id = :user_id AND
event_domain.key = :api_key AND
event_domain.checked IS FALSE'
);
return array_column($this->execQuery($query, $params), 'id');
}
}

View File

@@ -0,0 +1,252 @@
<?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 Models;
class Email extends \Models\BaseSql implements \Interfaces\FraudFlagUpdaterInterface {
use \Traits\Enrichment\Emails;
protected $DB_TABLE_NAME = 'event';
public function getEmailDetails(int $id, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':id' => $id,
];
$query = (
'SELECT
event_email.id AS email_id,
event_email.email,
event_email.lastseen AS email_lastseen,
event_email.created AS email_created,
event_email.data_breach,
event_email.data_breaches,
event_email.earliest_breach,
event_email.profiles,
event_email.blockemails,
event_email.domain_contact_email,
event_email.fraud_detected,
event_email.checked,
-- event_email.alert_list,
event_domain.id AS domain_id,
event_domain.domain,
event_domain.blockdomains,
event_domain.disposable_domains,
event_domain.total_visit,
event_domain.total_account,
event_domain.lastseen AS domain_lastseen,
event_domain.created AS domain_created,
event_domain.free_email_provider,
event_domain.tranco_rank,
event_domain.creation_date,
event_domain.expiration_date,
event_domain.return_code,
event_domain.closest_snapshot,
event_domain.mx_record,
event_domain.disabled
FROM
event_email
LEFT JOIN event_domain
ON event_email.domain = event_domain.id
WHERE
event_email.id = :id AND
event_email.key = :api_key'
);
$results = $this->execQuery($query, $params);
$this->calculateEmailReputation($results);
return $results[0] ?? [];
}
public function getIdByValue(string $email, int $apiKey): ?int {
$query = (
'SELECT
event_email.id
FROM
event_email
WHERE
event_email.key = :api_key
AND event_email.email = :email_value'
);
$params = [
':email_value' => $email,
':api_key' => $apiKey,
];
$results = $this->execQuery($query, $params);
return $results[0]['id'] ?? null;
}
public function getSeenInLastDay(
bool $includeAlertListed = false,
bool $includeWithoutHash = false,
bool $includeWithBlacklistSyncSkipped = false,
): array {
$params = [
':includeAlertListed' => $includeAlertListed,
':includeWithoutHash' => $includeWithoutHash,
':includeWithBlacklistSyncSkipped' => $includeWithBlacklistSyncSkipped,
];
$query = (
'SELECT
event_email.key,
event_email.email,
event_email.hash
FROM
event_email
JOIN
event_account ON event_email.account_id = event_account.id
JOIN
dshb_api ON event_account.key = dshb_api.id
WHERE
event_email.lastseen >= CURRENT_DATE - 1
AND (:includeAlertListed = TRUE OR event_email.alert_list != TRUE OR event_email.alert_list IS NULL)
AND (:includeWithoutHash = TRUE OR event_email.hash IS NOT NULL)
AND (:includeWithBlacklistSyncSkipped = TRUE OR dshb_api.skip_blacklist_sync != TRUE)'
);
return $this->execQuery($query, $params);
}
public function updateAlertListedByHashes(array $hashes, bool $alertListed, int $apiKey): void {
[$params, $placeHolders] = $this->getArrayPlaceholders($hashes);
$params[':alertListed'] = $alertListed;
$params[':key'] = $apiKey;
$query = (
"UPDATE event_email
SET alert_list = :alertListed
WHERE
key = :key AND
hash IN ({$placeHolders})"
);
$this->execQuery($query, $params);
}
public function updateFraudFlag(array $ids, bool $fraud, int $apiKey): void {
if (!count($ids)) {
return;
}
[$params, $placeHolders] = $this->getArrayPlaceholders($ids);
$params[':fraud'] = $fraud;
$params[':api_key'] = $apiKey;
$query = (
"UPDATE event_email
SET fraud_detected = :fraud
WHERE
key = :api_key
AND id IN ({$placeHolders})"
);
$this->execQuery($query, $params);
}
public function extractById(int $entityId, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':id' => $entityId,
];
$query = (
"SELECT
COALESCE(event_email.email, '') AS value,
event_email.hash AS hash
FROM
event_email
WHERE
event_email.key = :api_key
AND event_email.id = :id
LIMIT 1"
);
$results = $this->execQuery($query, $params);
return $results[0] ?? [];
}
public function countNotChecked(int $apiKey): int {
$params = [
':key' => $apiKey,
];
$query = (
'SELECT
COUNT(*) AS count
FROM event_email
WHERE
event_email.key = :key AND
event_email.checked IS FALSE'
);
$results = $this->execQuery($query, $params);
return $results[0]['count'] ?? 0;
}
public function notCheckedExists(int $apiKey): bool {
$params = [
':key' => $apiKey,
];
$query = (
'SELECT 1
FROM event_email
WHERE
event_email.key = :key AND
event_email.checked IS FALSE
LIMIT 1'
);
$results = $this->execQuery($query, $params);
return (bool) count($results);
}
public function notCheckedForUserId(int $userId, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':user_id' => $userId,
];
$query = (
'SELECT DISTINCT
event_email.id
FROM event_email
WHERE
event_email.account_id = :user_id AND
event_email.key = :api_key AND
event_email.checked IS FALSE'
);
return array_column($this->execQuery($query, $params), 'id');
}
}

View File

@@ -0,0 +1,90 @@
<?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 Models\Enrichment;
class Base {
public function queryParams(): array {
$properties = get_object_vars($this);
$modifiedArray = [];
foreach ($properties as $key => $value) {
$modifiedArray[':' . $key] = $value;
}
return $modifiedArray;
}
public function slimIds(array $ids): array {
$filtered = array_filter($ids, static function ($value): bool {
return $value !== null;
});
return array_unique($filtered);
}
public function updateStringByPlaceholders(array $placeholders): string {
$transformed = array_map(static function ($item): string {
$key = ltrim($item, ':');
return "{$key} = {$item}";
}, $placeholders);
return implode(', ', $transformed);
}
public function validateIP(string $ip): bool {
return filter_var($ip, FILTER_VALIDATE_IP) !== false;
}
// Validate date
public function validateDate(string $date, string $format = 'Y-m-d'): bool {
$d = \DateTime::createFromFormat($format, $date);
return $d && $d->format($format) === $date;
}
public function validateDates(array $dates): bool {
foreach ($dates as $date) {
if ($date !== null && !$this->validateDate($date)) {
return false;
}
}
return true;
}
public function validateCidr(string $cidr): bool {
$parts = explode('/', $cidr);
if (count($parts) !== 2) {
return false;
}
$ip = $parts[0];
$netmask = intval($parts[1]);
if ($netmask < 0) {
return false;
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $netmask <= 32;
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return $netmask <= 128;
}
return false;
}
}

View File

@@ -0,0 +1,69 @@
<?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 Models\Enrichment;
class Device extends \Models\Enrichment\Base {
protected string $ua;
protected ?string $device;
protected ?string $browser_name;
protected ?string $browser_version;
protected ?string $os_name;
protected ?string $os_version;
protected bool $modified;
protected bool $checked = true;
public function __construct() {
}
public function init(array $data): void {
$this->ua = $data['ua'];
$this->device = $data['device'];
$this->browser_name = $data['browser_name'];
$this->browser_version = $data['browser_version'];
$this->os_name = $data['os_name'];
$this->os_version = $data['os_version'];
$this->modified = $data['modified'];
}
public function prepareUpdate(): array {
$params = $this->queryParams();
unset($params[':ua']);
$placeholders = array_keys($params);
$updateString = $this->updateStringByPlaceholders($placeholders);
return [$params, $updateString];
}
public function updateEntityInDb(int $entityId, int $apiKey): void {
// total_visit and total_account should remain still
[$params, $updateString] = $this->prepareUpdate();
$params['entity_id'] = $entityId;
$params['key'] = $apiKey;
$query = ("
UPDATE event_ua_parsed
SET {$updateString}
WHERE
event_ua_parsed.id = :entity_id AND
event_ua_parsed.key = :key
");
$model = new \Models\Device();
$model->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,96 @@
<?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 Models\Enrichment;
class DomainFound extends \Models\Enrichment\Base {
protected string $domain;
protected bool $blockdomains;
protected bool $disposable_domains;
protected bool $free_email_provider;
protected ?string $creation_date; // date
protected ?string $expiration_date; // date
protected ?int $return_code;
protected bool $disabled;
protected ?string $closest_snapshot; // date
protected bool $mx_record;
protected ?string $ip; // ipvanyaddress
protected ?string $geo_ip;
protected ?string $geo_html;
protected ?string $web_server;
protected ?string $hostname;
protected ?string $emails;
protected ?string $phone;
protected string $discovery_date; // date
protected ?int $tranco_rank;
protected bool $checked = true;
public function init(array $data): void {
$this->domain = $data['domain'];
$this->blockdomains = $data['blockdomains'];
$this->disposable_domains = $data['disposable_domains'];
$this->free_email_provider = $data['free_email_provider'];
$this->creation_date = $data['creation_date'];
$this->expiration_date = $data['expiration_date'];
$this->return_code = $data['return_code'];
$this->disabled = $data['disabled'];
$this->closest_snapshot = $data['closest_snapshot'];
$this->mx_record = $data['mx_record'];
$this->ip = $data['ip'];
$this->geo_ip = $data['geo_ip'];
$this->geo_html = $data['geo_html'];
$this->web_server = $data['web_server'];
$this->hostname = $data['hostname'];
$this->emails = $data['emails'];
$this->phone = $data['phone'];
$this->discovery_date = $data['discovery_date'];
$this->tranco_rank = $data['tranco_rank'];
$dates = [$this->creation_date, $this->expiration_date, $this->closest_snapshot, $this->discovery_date];
if (($this->ip && !$this->validateIP($this->ip)) || !$this->validateDates($dates)) {
throw new \Exception('Validation failed');
}
}
public function prepareUpdate(): array {
$params = $this->queryParams();
unset($params[':domain']);
$placeholders = array_keys($params);
$updateString = $this->updateStringByPlaceholders($placeholders);
return [$params, $updateString];
}
public function updateEntityInDb(int $entityId, int $apiKey): void {
// total_visit and total_account should remain still
[$params, $updateString] = $this->prepareUpdate();
$params['entity_id'] = $entityId;
$params['key'] = $apiKey;
$query = ("
UPDATE event_domain
SET {$updateString}
WHERE
event_domain.id = :entity_id AND
event_domain.key = :key
");
$model = new \Models\Domain();
$model->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,79 @@
<?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 Models\Enrichment;
class DomainNotFound extends \Models\Enrichment\Base {
protected string $domain;
protected bool $blockdomains;
protected bool $disposable_domains;
protected bool $free_email_provider;
protected ?string $creation_date; // date
protected ?string $expiration_date; // date
protected ?int $return_code;
protected bool $disabled;
protected ?string $closest_snapshot; // date
protected bool $mx_record;
protected bool $checked = true;
public function __construct() {
}
public function init(array $data): void {
$this->domain = $data['domain'];
$this->blockdomains = $data['blockdomains'];
$this->disposable_domains = $data['disposable_domains'];
$this->free_email_provider = $data['free_email_provider'];
$this->creation_date = $data['creation_date'];
$this->expiration_date = $data['expiration_date'];
$this->return_code = $data['return_code'];
$this->disabled = $data['disabled'];
$this->closest_snapshot = $data['closest_snapshot'];
$this->mx_record = $data['mx_record'];
if (!$this->validateDates([$this->creation_date, $this->expiration_date, $this->closest_snapshot])) {
throw new \Exception('Validation failed');
}
}
public function prepareUpdate(): array {
$params = $this->queryParams();
unset($params[':domain']);
$placeholders = array_keys($params);
$updateString = $this->updateStringByPlaceholders($placeholders);
return [$params, $updateString];
}
public function updateEntityInDb(int $entityId, int $apiKey): void {
// total_visit and total_account should remain still
[$params, $updateString] = $this->prepareUpdate();
$params['entity_id'] = $entityId;
$params['key'] = $apiKey;
$query = ("
UPDATE event_domain
SET {$updateString}
WHERE
event_domain.id = :entity_id AND
event_domain.key = :key
");
$model = new \Models\Domain();
$model->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,84 @@
<?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 Models\Enrichment;
class Email extends \Models\Enrichment\Base {
protected string $email;
protected bool $blockemails;
protected bool $data_breach;
protected int $data_breaches;
protected ?string $earliest_breach;
protected int $profiles;
protected bool $domain_contact_email;
protected string $domain;
protected ?bool $alert_list;
protected bool $checked = true;
public function __construct() {
}
public function init(array $data): void {
$this->email = $data['email'];
$this->blockemails = $data['blockemails'];
$this->data_breach = $data['data_breach'];
$this->data_breaches = $data['data_breaches'];
$this->earliest_breach = $data['earliest_breach'];
$this->profiles = $data['profiles'];
$this->domain_contact_email = $data['domain_contact_email'];
$this->domain = $data['domain'];
$this->alert_list = $data['alert_list'];
if (!$this->validateDates([$this->earliest_breach])) {
throw new \Exception('Validation failed');
}
}
public function prepareUpdate(): array {
$params = $this->queryParams();
unset($params[':email']);
// !
unset($params[':domain']);
// if new alert_list is null -- don't override
if ($params[':alert_list'] === null) {
unset($params[':alert_list']);
}
$placeholders = array_keys($params);
$updateString = $this->updateStringByPlaceholders($placeholders);
return [$params, $updateString];
}
public function updateEntityInDb(int $entityId, int $apiKey): void {
[$params, $updateString] = $this->prepareUpdate();
$params['entity_id'] = $entityId;
$params['key'] = $apiKey;
// other params will stay still
$query = ("
UPDATE event_email
SET {$updateString}
WHERE
event_email.id = :entity_id AND
event_email.key = :key
");
$model = new \Models\Device();
$model->execQuery($query, $params);
}
}

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)
*/
namespace Models\Enrichment;
class Ip extends \Models\Enrichment\Base {
protected string $ip; // ipvanyaddress
protected string $country;
protected ?int $asn;
protected ?string $name;
protected bool $hosting;
protected bool $vpn;
protected bool $tor;
protected bool $relay;
protected bool $starlink;
protected ?string $description;
protected bool $blocklist;
protected array $domains_count; // list[str]
protected string $cidr; // ipvanynetwork
protected ?bool $alert_list;
protected bool $checked = true;
protected ?int $isp;
public function __construct() {
// empty
}
public function init(array $data): void {
$this->ip = $data['ip'];
$this->country = $data['country'];
$this->asn = $data['asn'];
$this->name = $data['name'];
$this->hosting = $data['hosting'];
$this->vpn = $data['vpn'];
$this->tor = $data['tor'];
$this->relay = $data['relay'];
$this->starlink = $data['starlink'];
$this->description = $data['description'];
$this->blocklist = $data['blocklist'];
$this->domains_count = $data['domains_count'];
$this->cidr = $data['cidr'];
$this->alert_list = $data['alert_list'];
if (!$this->validateIP($this->ip) || !$this->validateCIDR($this->cidr)) {
throw new \Exception('Validation failed');
}
}
public function prepareUpdate(): array {
$params = $this->queryParams();
unset($params[':ip']);
$params[':domains_count'] = json_encode($params[':domains_count']);
$params[':data_center'] = $params[':hosting'];
unset($params[':hosting']);
// if new alert_list is null -- don't override
if ($params[':alert_list'] === null) {
unset($params[':alert_list']);
}
// set $params[':isp'] later
unset($params[':asn']);
unset($params[':name']);
unset($params[':description']);
$placeholders = array_keys($params);
$updateString = $this->updateStringByPlaceholders($placeholders);
return [$params, $updateString];
}
// TODO: update countries table counters
public function updateEntityInDb(int $entityId, int $apiKey): void {
$ipModel = new \Models\Ip();
$previousIpData = $ipModel->getFullIpInfoById($entityId);
$previousIspId = count($previousIpData) ? $previousIpData['ispid'] : null;
$previousCountryId = count($previousIpData) ? $previousIpData['country_id'] : 0;
// get current isp id
$this->name = $this->asn !== null ? $this->name : 'N/A';
$this->asn = $this->asn !== null ? $this->asn : 64496;
$ispModel = new \Models\Isp();
$newIspId = $ispModel->getIdByAsn($this->asn, $apiKey);
$newIspData = [
'asn' => $this->asn,
'name' => $this->name,
'description' => $this->description,
];
$newIspModel = new \Models\Enrichment\Isp();
$newIspModel->init($newIspData);
// new isp is not in db
if ($newIspId === null) {
$newIspData['lastseen'] = $previousIpData['lastseen'];
$newIspData['created'] = $previousIpData['created'];
$newIspId = $ispModel->insertRecord($newIspData, $apiKey);
} else {
$newIspModel->updateEntityInDb($newIspId, $apiKey);
}
$this->isp = $newIspId;
$countryModel = new \Models\Country();
$newCountryId = $countryModel->getCountryIdByIso($this->country);
$countryRecord = $countryModel->getCountryById($newCountryId, $apiKey);
if (!count($countryRecord)) {
$newCountryData = [
'id' => $newCountryId,
'lastseen' => $previousIpData['lastseen'],
'created' => $previousIpData['created'],
];
$countryModel->insertRecord($newCountryData, $apiKey);
}
// total_visit and total_account should remain still
[$params, $updateString] = $this->prepareUpdate();
$params[':country'] = $newCountryId;
$params[':entity_id'] = $entityId;
$params[':key'] = $apiKey;
$query = ("
UPDATE event_ip
SET {$updateString}
WHERE
event_ip.id = :entity_id AND
event_ip.key = :key
");
$model = new \Models\Ip();
$model->execQuery($query, $params);
// update totals only after event_ip update!
$ispIds = $this->slimIds([$previousIspId, $newIspId]);
$ispModel->updateTotalsByEntityIds($ispIds, $apiKey, true);
$countryIds = $this->slimIds([$previousCountryId, $newCountryId]);
$countryModel->updateTotalsByEntityIds($countryIds, $apiKey, true);
}
}

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)
*/
namespace Models\Enrichment;
class Isp extends \Models\Enrichment\Base {
protected ?int $asn;
protected ?string $name;
protected ?string $description;
public function __construct() {
}
public function init(array $data): void {
$this->asn = $data['asn'];
$this->name = $data['name'];
$this->description = $data['description'];
}
public function prepareUpdate(): array {
$params = $this->queryParams();
unset($params[':asn']);
$placeholders = array_keys($params);
$updateString = $this->updateStringByPlaceholders($placeholders);
return [$params, $updateString];
}
public function updateEntityInDb(int $entityId, int $apiKey): void {
[$params, $updateString] = $this->prepareUpdate();
$params[':entity_id'] = $entityId;
$params[':key'] = $apiKey;
$query = ("
UPDATE event_isp
SET {$updateString}
WHERE
event_isp.id = :entity_id AND
event_isp.key = :key
");
$model = new \Models\Isp();
$model->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,119 @@
<?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 Models\Enrichment;
class LocalhostIp extends \Models\Enrichment\Base {
protected string $ip; // ipvanyaddress
protected int $country = 0;
protected ?int $asn = 0;
protected string $name = 'Local area network';
protected ?string $description = null;
protected bool $checked = true;
protected ?int $isp;
public function __construct() {
// empty
}
public function init(array $data): void {
$this->ip = $data['value'];
if (!$this->validateIP($this->ip) || $data['error'] !== \Utils\Constants::get('ENRICHMENT_IP_IS_BOGON')) {
throw new \Exception('Validation failed');
}
}
public function prepareUpdate(): array {
$params = $this->queryParams();
unset($params[':ip']);
// set $params[':isp'] later
unset($params[':asn']);
unset($params[':name']);
unset($params[':description']);
$placeholders = array_keys($params);
$updateString = $this->updateStringByPlaceholders($placeholders);
return [$params, $updateString];
}
// TODO: update countries table counters
public function updateEntityInDb(int $entityId, int $apiKey): void {
$ipModel = new \Models\Ip();
$previousIpData = $ipModel->getFullIpInfoById($entityId);
$previousIspId = count($previousIpData) ? $previousIpData['ispid'] : null;
$previousCountryId = count($previousIpData) ? $previousIpData['country_id'] : 0;
// get current isp id
$ispModel = new \Models\Isp();
$newIspId = $ispModel->getIdByAsn($this->asn, $apiKey);
$newIspData = [
'asn' => $this->asn,
'name' => $this->name,
'description' => $this->description,
];
$newIspModel = new \Models\Enrichment\Isp();
$newIspModel->init($newIspData);
// new isp is not in db
if ($newIspId === null) {
$newIspData['lastseen'] = $previousIpData['lastseen'];
$newIspId = $ispModel->insertRecord($newIspData, $apiKey);
} else {
$newIspModel->updateEntityInDb($newIspId, $apiKey);
}
$this->isp = $newIspId;
$countryModel = new \Models\Country();
$newCountryId = $countryModel->getCountryIdByIso($this->country);
$countryRecord = $countryModel->getCountryById($newCountryId, $apiKey);
if (!count($countryRecord)) {
$newCountryData = [
'id' => $newCountryId,
'lastseen' => $previousIpData['lastseen'],
];
$countryModel->insertRecord($newCountryData, $apiKey);
}
// total_visit and total_account should remain still
[$params, $updateString] = $this->prepareUpdate();
$params[':country'] = $newCountryId;
$params[':entity_id'] = $entityId;
$params[':key'] = $apiKey;
$query = ("
UPDATE event_ip
SET {$updateString}
WHERE
event_ip.id = :entity_id AND
event_ip.key = :key
");
$model = new \Models\Ip();
$model->execQuery($query, $params);
// update totals only after event_ip update!
$ispIds = $this->slimIds([$previousIspId, $newIspId]);
$ispModel->updateTotalsByEntityIds($ispIds, $apiKey, true);
$countryIds = $this->slimIds([$previousCountryId, $newCountryId]);
$countryModel->updateTotalsByEntityIds($countryIds, $apiKey, true);
}
}

View File

@@ -0,0 +1,70 @@
<?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 Models\Enrichment;
class PhoneInvalid extends \Models\Enrichment\Base {
protected string $phone_number;
protected bool $invalid;
protected string $validation_errors;
//protected ?bool $alert_list = null;
protected bool $checked = true;
protected int $country_code;
public function __construct() {
}
public function init(array $data): void {
$this->phone_number = $data['phone_number'];
$this->invalid = $data['invalid'];
$this->validation_errors = $data['validation_error'];
$this->country_code = 0;
if (!$this->invalid) {
throw new \Exception('Validation failed');
}
}
public function prepareUpdate(): array {
$params = $this->queryParams();
unset($params[':phone_number']);
$params[':validation_errors'] = json_encode($params[':validation_errors']);
$placeholders = array_keys($params);
$updateString = $this->updateStringByPlaceholders($placeholders);
return [$params, $updateString];
}
public function updateEntityInDb(int $entityId, int $apiKey): void {
[$params, $updateString] = $this->prepareUpdate();
$params['entity_id'] = $entityId;
$params['key'] = $apiKey;
// other params will stay still
$query = ("
UPDATE event_phone
SET {$updateString}
WHERE
event_phone.id = :entity_id AND
event_phone.key = :key
");
$model = new \Models\Phone();
$model->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,93 @@
<?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 Models\Enrichment;
class PhoneValid extends \Models\Enrichment\Base {
protected string $phone_number;
protected int $profiles;
protected ?string $iso_country_code;
protected int $calling_country_code;
protected string $national_format;
protected bool $invalid;
protected ?string $validation_errors;
protected ?string $carrier_name;
protected string $type;
protected ?bool $alert_list;
protected bool $checked = true;
protected int $country_code;
public function __construct() {
}
public function init(array $data): void {
$this->phone_number = $data['phone_number'];
$this->profiles = $data['profiles'];
$this->iso_country_code = $data['iso_country_code'];
$this->calling_country_code = $data['calling_country_code'];
$this->national_format = $data['national_format'];
$this->invalid = $data['invalid'];
$this->validation_errors = $data['validation_error'];
$this->carrier_name = $data['carrier_name'];
$this->type = $data['type'];
$this->alert_list = $data['alert_list'];
if ($this->invalid || $this->validation_errors !== null) {
throw new \Exception('Validation failed');
}
}
public function prepareUpdate(): array {
$params = $this->queryParams();
unset($params[':phone_number']);
$params[':validation_errors'] = json_encode($params[':validation_errors']);
// if new alert_list is null -- don't override
if ($params[':alert_list'] === null) {
unset($params[':alert_list']);
}
$placeholders = array_keys($params);
$updateString = $this->updateStringByPlaceholders($placeholders);
return [$params, $updateString];
}
public function updateEntityInDb(int $entityId, int $apiKey): void {
$this->country_code = 0;
if ($this->iso_country_code !== null) {
$countryModel = new \Models\Country();
$this->country_code = $countryModel->getCountryIdByIso($this->iso_country_code);
}
[$params, $updateString] = $this->prepareUpdate();
$params['entity_id'] = $entityId;
$params['key'] = $apiKey;
$query = ("
UPDATE event_phone
SET {$updateString}
WHERE
event_phone.id = :entity_id AND
event_phone.key = :key
");
$model = new \Models\Phone();
$model->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,235 @@
<?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 Models;
class Event extends \Models\BaseSql {
use \Traits\Enrichment\Ips;
use \Traits\Enrichment\Emails;
use \Traits\Enrichment\TimeZones;
protected $DB_TABLE_NAME = 'event';
public function getLastEvent(int $apiKey): array {
$params = [
':api_key' => $apiKey,
];
$query = (
'SELECT
event.id,
event.time
FROM
event
WHERE
event.key = :api_key
ORDER BY id DESC
LIMIT 1 OFFSET 0'
);
return $this->execQuery($query, $params);
}
public function getEventDetails(int $id, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':id' => $id,
];
$query = (
'SELECT
event.id,
event.time AS event_time,
event.http_code AS event_http_code,
event.type AS event_type_id,
event_ip.ip,
event_ip.cidr,
event_ip.data_center,
event_ip.vpn,
event_ip.tor,
event_ip.relay,
event_ip.starlink,
event_ip.id AS ipId,
event_ip.blocklist AS blocklist,
event_ip.alert_list AS ip_alert_list,
event_ip.shared AS ip_users,
event_ip.total_visit AS ip_events,
event_ip.checked,
event_ip.fraud_detected AS fraud_detected,
event_isp.asn,
event_isp.description,
event_isp.id AS ispId,
event_isp.name AS netname,
event_url.url,
event_url.title,
event_url_query.query,
event_url.id AS url_id,
event_referer.referer,
event_account.score,
event_account.score_updated_at,
event_account.fraud,
event_account.reviewed,
event_account.added_to_review,
event_account.firstname,
event_account.lastname,
event_account.is_important,
event_account.latest_decision,
event_account.id AS accountid,
event_account.userid AS accounttitle,
event_phone.phone_number AS phonenumber,
event_phone.type AS phone_type,
event_phone.carrier_name,
event_phone.profiles AS phone_profiles,
event_phone.invalid AS phone_invalid,
event_phone.shared AS phone_users,
event_phone.alert_list AS phone_alert_list,
event_phone.fraud_detected AS phone_fraud_detected,
phone_countries.id AS phone_country_id,
phone_countries.iso AS phone_country_iso,
phone_countries.value AS phone_full_country,
event_email.email,
event_email.profiles,
event_email.data_breach,
event_email.data_breaches,
event_email.earliest_breach AS email_earliest_breach,
event_email.blockemails AS blockemails,
event_email.alert_list AS email_alert_list,
event_email.fraud_detected AS email_fraud_detected,
current_email.email AS current_email,
event_domain.id AS domainId,
event_domain.domain,
event_domain.tranco_rank,
event_domain.blockdomains,
event_domain.disposable_domains,
event_domain.free_email_provider,
event_domain.creation_date AS domain_creation_date,
event_domain.disabled AS domain_disabled,
event_domain.expiration_date AS domain_expiration_date,
event_domain.return_code AS domain_return_code,
ip_countries.id AS country_id,
ip_countries.id AS ip_country_id,
ip_countries.iso AS ip_country_iso,
ip_countries.value AS ip_full_country,
event_device.lang,
event_device.user_agent AS deviceId,
event_device.created AS device_created,
event_ua_parsed.ua,
event_ua_parsed.os_name,
event_ua_parsed.os_version,
event_ua_parsed.browser_name,
event_ua_parsed.browser_version,
event_ua_parsed.device AS device_name,
event_ua_parsed.modified AS ua_modified,
event_type.name AS event_type_name,
event_type.value AS event_type,
event_payload.payload AS event_payload,
event_http_method.name AS event_http_method_name
FROM
event
INNER JOIN event_account
ON (event.account = event_account.id)
INNER JOIN event_url
ON (event.url = event_url.id)
LEFT JOIN event_referer
ON (event.referer = event_referer.id)
FULL OUTER JOIN event_url_query
ON (event.query = event_url_query.id)
INNER JOIN event_device
ON (event.device = event_device.id)
INNER JOIN event_type
ON (event.type = event_type.id)
INNER JOIN event_ua_parsed
ON (event_device.user_agent = event_ua_parsed.id)
INNER JOIN event_ip
ON (event.ip = event_ip.id)
LEFT JOIN event_isp
ON (event_ip.isp = event_isp.id)
INNER JOIN countries AS ip_countries
ON (event_ip.country = ip_countries.id)
LEFT JOIN event_email
ON (event.email = event_email.id)
LEFT JOIN event_email AS current_email
ON (event_account.lastemail = current_email.id)
LEFT JOIN event_domain
ON (event_email.domain = event_domain.id)
LEFT JOIN event_phone
ON (event_account.lastphone = event_phone.id)
LEFT JOIN countries AS phone_countries
ON (phone_countries.id = event_phone.country_code)
LEFT JOIN event_http_method
ON (event.http_method = event_http_method.id)
LEFT JOIN event_payload
ON (event.payload = event_payload.id)
WHERE
event.key = :api_key
AND event.id = :id
LIMIT 1'
);
$results = $this->execQuery($query, $params);
$this->calculateIpType($results);
$this->calculateEmailReputation($results);
//$this->translateTimeZones($results, ['event_time', 'domain_creation_date']);
if (count($results)) {
$results = $results[0];
$spamlist = $results['ip_type'] === 'Spam list';
$results['spamlist'] = $spamlist;
$model = new \Models\User();
$results['score_details'] = $model->getApplicableRulesByAccountId($results['accountid'], $apiKey, true);
$results['score_calculated'] = $results['score'] !== null;
}
return $results;
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Models;
class EventType extends \Models\BaseSql {
protected $DB_TABLE_NAME = 'event_type';
public function getAll(): array {
$query = 'SELECT id, value, name FROM event_type';
return $this->execQuery($query, null);
}
}

View File

@@ -0,0 +1,286 @@
<?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 Models;
class Events extends \Models\BaseSql {
use \Traits\DateRange;
use \Traits\Enrichment\Ips;
protected $DB_TABLE_NAME = 'event';
public function getDistinctAccounts(int $after, int $to): array {
$params = [
':cursor' => $after,
':next_cursor' => $to,
];
$query = (
'SELECT DISTINCT
event.account AS "accountId",
event.key
FROM
event
JOIN
event_account ON event.account = event_account.id
WHERE
event.id > :cursor
AND event.id <= :next_cursor'
);
return $this->execQuery($query, $params);
}
private function getEvents(array $queryParams): array {
$request = $this->f3->get('REQUEST');
$dateRange = $this->getDatesRange($request);
$data = $this->getData($queryParams, $dateRange);
$total = $this->getTotal($queryParams, $dateRange);
return [
'draw' => $request['draw'] ?? 1,
'recordsTotal' => $total,
'recordsFiltered' => $total,
'data' => $data,
];
}
public function getEventsByUser(int $userId, int $apiKey): array {
$params = [
':user_id' => $userId,
':api_key' => $apiKey,
];
return $this->getEvents($params);
}
private function getData(array $params, ?array $dateRange): array {
$query = (
'SELECT
event.id,
event.time,
event.http_code,
event_ip.ip,
event_ip.data_center,
-- event_ip.asn,
event_ip.vpn,
event_ip.tor,
event_ip.relay,
event_ip.starlink,
event_ip.id AS ipId,
-- event_ip.description,
-- event_ip.name as netname,
event_ip.blocklist,
event_ip.fraud_detected,
event_ip.checked,
event_isp.asn,
event_isp.description,
event_isp.name AS netname,
event_url.url,
event_url.id as url_id,
event_url_query.query,
event_url.title,
event_referer.referer,
-- event_account.is_important,
event_account.id AS accountid,
event_account.userid AS accounttitle,
event_account.score,
countries.id AS country_id,
countries.iso AS country_iso,
countries.value AS full_country,
event_device.id AS deviceId,
event_ua_parsed.device,
event_ua_parsed.ua,
event_ua_parsed.browser_name,
event_ua_parsed.browser_version,
event_ua_parsed.os_name,
event_ua_parsed.os_version,
event_ua_parsed.ua,
event_type.value AS event_type,
event_type.name AS event_type_name
FROM
event
INNER JOIN event_account
ON (event.account = event_account.id)
INNER JOIN event_url
ON (event.url = event_url.id)
LEFT JOIN event_referer
ON (event.referer = event_referer.id)
-- FULL OUTER JOIN event_url_query
LEFT JOIN event_url_query
ON (event.query = event_url_query.id)
INNER JOIN event_device
ON (event.device = event_device.id)
INNER JOIN event_type
ON (event.type = event_type.id)
INNER JOIN event_ua_parsed
ON (event_device.user_agent = event_ua_parsed.id)
INNER JOIN event_ip
ON (event.ip = event_ip.id)
LEFT JOIN event_isp
ON (event_ip.isp = event_isp.id)
INNER JOIN countries
ON (event_ip.country = countries.id)
WHERE
event.account = :user_id AND
event.key = :api_key
%s
ORDER BY
event.time DESC'
);
$this->applyDateRange($query, $params, $dateRange);
$this->applyLimit($query, $params);
$results = $this->execQuery($query, $params);
$this->calculateIpType($results);
return $results;
}
private function getTotal(array $params, ?array $dateRange): int {
$query = (
'SELECT
COUNT(event.id)
FROM
event
INNER JOIN event_account
ON (event.account = event_account.id)
INNER JOIN event_url
ON (event.url = event_url.id)
FULL OUTER JOIN event_url_query
ON (event.query = event_url_query.id)
INNER JOIN event_device
ON (event.device = event_device.id)
INNER JOIN event_ua_parsed
ON (event_device.user_agent = event_ua_parsed.id)
INNER JOIN event_ip
ON (event.ip = event_ip.id)
INNER JOIN countries
ON (event_ip.country = countries.id)
WHERE
event.account = :user_id AND
event.key = :api_key
%s'
);
$this->applyDateRange($query, $params, $dateRange);
$results = $this->execQuery($query, $params);
return $results[0]['count'];
}
private function applyDateRange(string &$query, array &$params, ?array $dateRange = null): void {
$searchConditions = '';
if ($dateRange) {
$searchConditions = (
'AND event.time >= :start_time
AND event.time <= :end_time'
);
$params[':end_time'] = $dateRange['endDate'];
$params[':start_time'] = $dateRange['startDate'];
}
$query = sprintf($query, $searchConditions);
}
protected function applyLimit(string &$query, array &$queryParams): void {
$request = $this->f3->get('REQUEST');
$start = $request['start'] ?? null;
$length = $request['length'] ?? null;
if (isset($start) && isset($length)) {
$query .= ' LIMIT :length OFFSET :start';
$queryParams[':start'] = $start;
$queryParams[':length'] = $length;
}
}
public function uniqueEntitesByUserId(int $userId, int $apiKey): array {
$params = [
':api_key' => $apiKey,
':user_id' => $userId,
];
$query = (
'SELECT DISTINCT
event.ip,
event_ip.isp,
event_ip.country,
event.url,
event_email.domain,
event_phone.phone_number
FROM
event
LEFT JOIN event_ip
ON event.ip = event_ip.id
LEFT JOIN event_email
ON event.email = event_email.id
LEFT JOIN event_phone
ON event.phone = event_phone.id
LEFT JOIN event_country
ON event_ip.country = event_country.country AND event_ip.key = event_country.key
WHERE
event.key = :api_key AND
event.account = :user_id'
);
$results = $this->execQuery($query, $params);
return [
'ip_ids' => array_unique(array_column($results, 'ip')),
'isp_ids' => array_unique(array_column($results, 'isp')),
'country_ids' => array_unique(array_column($results, 'country')),
'url_ids' => array_unique(array_column($results, 'url')),
'domain_ids' => array_unique(array_column($results, 'domain')),
'phone_numbers' => array_unique(array_column($results, 'phone_number')),
];
}
public function retentionDeletion(int $weeks, int $apiKey): int {
// insuring clause
if ($weeks < 1) {
return 0;
}
$params = [
':api_key' => $apiKey,
':weeks' => $weeks,
];
$query = (
'DELETE FROM event
WHERE
event.key = :api_key AND
(EXTRACT(EPOCH FROM (NOW() - event.time)) / (60 * 60 * 24 * 7)) >= :weeks'
);
return $this->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,72 @@
<?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 Models;
class FieldAuditTrail extends \Models\BaseSql {
protected $DB_TABLE_NAME = 'event_field_audit_trail';
public function getByEventId(int $eventId, int $apiKey): array {
$params = [
':event_id' => $eventId,
':api_key' => $apiKey,
];
$query = (
'SELECT
event_field_audit_trail.field_id,
event_field_audit_trail.field_name,
event_field_audit_trail.old_value,
event_field_audit_trail.new_value,
event_field_audit_trail.parent_id,
event_field_audit_trail.parent_name
FROM
event_field_audit_trail
WHERE
event_field_audit_trail.event_id = :event_id AND
event_field_audit_trail.key = :api_key
ORDER BY id DESC'
);
return $this->execQuery($query, $params);
}
public function getByUserId(int $userId, int $apiKey): array {
$params = [
':user_id' => $userId,
':api_key' => $apiKey,
];
$query = (
'SELECT
event_field_audit_trail.field_id,
event_field_audit_trail.field_name,
event_field_audit_trail.old_value,
event_field_audit_trail.new_value,
event_field_audit_trail.parent_id,
event_field_audit_trail.parent_name
FROM
event_field_audit_trail
WHERE
event_field_audit_trail.account_id = :user_id AND
event_field_audit_trail.key = :api_key
ORDER BY id DESC'
);
return $this->execQuery($query, $params);
}
}

View File

@@ -0,0 +1,56 @@
<?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 Models;
class ForgotPassword extends \Models\BaseSql {
protected $DB_TABLE_NAME = 'dshb_operators_forgot_password';
public function add(int $operatorId): void {
$record = $this->getUnusedKeyByOperatorId($operatorId);
if ($record) {
$this->status = 'invalidated';
$this->save();
}
$this->reset();
$this->renew_key = $this->getPseudoRandomString(32);
$this->operator_id = $operatorId;
$this->status = 'unused';
$this->save();
}
private function getUnusedKeyByOperatorId(int $operatorId): self|null|false {
return $this->load(
['"operator_id"=? AND "status"=?', $operatorId, 'unused'],
);
}
public function getUnusedByRenewKey(string $key): self|null|false {
return $this->load(
['"renew_key"=? AND "status"=?', $key, 'unused'],
);
}
public function deactivate(): void {
if ($this->loaded()) {
$this->status = 'used';
$this->save();
}
}
}

View File

@@ -0,0 +1,78 @@
<?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 Models\Grid\Base;
class Grid extends \Models\BaseSql {
use \Traits\Enrichment\TimeZones;
use \Traits\DateRange;
protected $DB_TABLE_NAME = 'event';
protected $idsModel = null;
protected $apiKey = null;
protected $query = null;
protected function getGrid(?string $ids = null, array $idsParams = []): array {
$this->setIds($ids, $idsParams);
$draw = $this->f3->get('REQUEST.draw');
$draw = $draw ?? 1;
$data = $this->getData();
$total = $this->getTotal();
$params = $this->f3->get('GET');
$dateRange = $this->getDatesRange($params);
return [
'data' => $data,
'draw' => $draw,
'recordsTotal' => $total,
'recordsFiltered' => $total,
'dateRange' => $dateRange,
];
}
public function setIds(?string $ids, array $idsParams): void {
$this->query->setIds($ids, $idsParams);
}
protected function getData(): array {
[$query, $params] = $this->query->getData();
$results = $this->execQuery($query, $params);
$this->convertTimeToUserTimezone($results);
$this->calculateCustomParams($results);
return $results;
}
protected function getTotal(): int {
[$query, $params] = $this->query->getTotal();
$results = $this->execQuery($query, $params);
return $results[0]['count'];
}
protected function convertTimeToUserTimezone(array &$result): void {
$this->translateTimeZones($result);
}
protected function calculateCustomParams(array &$result): void {
}
}

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)
*/
namespace Models\Grid\Base;
class Ids extends \Models\BaseSql {
protected $DB_TABLE_NAME = 'event';
private $apiKey = null;
public function __construct(int $apiKey) {
parent::__construct();
$this->apiKey = $apiKey;
}
public function execute(string $query, array $params): array {
$params[':api_key'] = $this->apiKey;
$data = $this->execQuery($query, $params);
$results = array_column($data, 'itemid');
return count($results) ? $results : [-1];
}
}

View File

@@ -0,0 +1,127 @@
<?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 Models\Grid\Base;
class Query {
use \Traits\Debug;
use \Traits\DateRange;
protected $f3 = null;
protected $apiKey = null;
protected $ids = null;
protected $idsParams = [];
protected $itemKey = null;
protected $itemId = null;
protected $defaultOrder = null;
protected $dateRangeField = 'event_country.lastseen';
protected $allowedColumns = [];
public function __construct(int $apiKey) {
$this->f3 = \Base::instance();
$this->apiKey = $apiKey;
}
public function setIds(?string $ids, array $idsParams): void {
$this->ids = $ids;
$this->idsParams = $idsParams;
if (count($this->idsParams)) {
$this->itemKey = array_keys($this->idsParams)[0];
$this->itemId = $this->idsParams[$this->itemKey];
}
}
protected function applyOrder(string &$query): void {
$request = $this->f3->get('REQUEST');
$order = $request['order'] ?? [];
$columns = $request['columns'] ?? [];
$orderCondition = $this->defaultOrder;
if (count($order) && count($columns)) {
$orderClauses = [];
foreach ($order as $orderData) {
$sortDirection = $orderData['dir'] === 'asc' ? 'ASC' : 'DESC';
$columnIndex = $orderData['column'];
$sortColumn = $columns[$columnIndex]['data'];
if (in_array($sortColumn, $this->allowedColumns)) {
$orderClauses[] = sprintf('%s %s', $sortColumn, $sortDirection);
}
}
if (count($orderClauses)) {
$orderCondition = implode(', ', $orderClauses);
}
}
if ($orderCondition) {
$query .= sprintf(' ORDER BY %s', $orderCondition);
}
}
protected function applyDateRange(string &$query, array &$queryParams): void {
$params = $this->f3->get('GET');
$dateRange = $this->getDatesRange($params);
if ($dateRange) {
$searchConditions = (
"AND {$this->dateRangeField} >= :start_time
AND {$this->dateRangeField} <= :end_time
%s"
);
$query = sprintf($query, $searchConditions);
$queryParams[':end_time'] = $dateRange['endDate'];
$queryParams[':start_time'] = $dateRange['startDate'];
}
}
protected function applyLimit(string &$query, array &$queryParams): void {
$request = $this->f3->get('REQUEST');
$start = $request['start'] ?? null;
$length = $request['length'] ?? null;
if (isset($start) && isset($length)) {
$query .= ' LIMIT :length OFFSET :start';
$queryParams[':start'] = $start;
$queryParams[':length'] = $length;
}
}
protected function getQueryParams(): array {
return [':api_key' => $this->apiKey];
}
public function injectIdQuery(string $field, &$params): string {
$idsQuery = $this->ids;
if ($idsQuery === null || $idsQuery === '') {
return '';
}
$idsParams = $this->idsParams;
foreach ($idsParams as $key => $value) {
if (!array_key_exists($key, $params) || $params[$key] === null) {
$params[$key] = $value;
}
}
return " AND $field IN ($idsQuery)";
}
}

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)
*/
namespace Models\Grid\Blacklist;
class Grid extends \Models\Grid\Base\Grid {
public function __construct(int $apiKey) {
parent::__construct();
$this->apiKey = $apiKey;
$this->idsModel = new Ids($apiKey);
$this->query = new Query($apiKey);
}
public function getAllBlacklistedItems(): array {
return $this->getGrid();
}
protected function convertTimeToUserTimezone(array &$result): void {
$fields = ['created', 'score_updated_at'];
$this->translateTimeZones($result, $fields);
}
}

View File

@@ -0,0 +1,19 @@
<?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 Models\Grid\Blacklist;
class Ids extends \Models\Grid\Base\Ids {
}

View File

@@ -0,0 +1,234 @@
<?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 Models\Grid\Blacklist;
class Query extends \Models\Grid\Base\Query {
protected $defaultOrder = 'created DESC, type ASC, value ASC';
protected $dateRangeField = 'blacklist.created';
protected $allowedColumns = ['score', 'created', 'type', 'value'];
public function getData(): array {
$queryParams = $this->getQueryParams();
$query = ("
SELECT DISTINCT
blacklist.is_important,
blacklist.accountid,
blacklist.accounttitle,
blacklist.created,
blacklist.score_updated_at,
blacklist.score,
blacklist.account_email AS email,
extra.type,
TRUE AS fraud,
CASE extra.type
WHEN 'ip' THEN blacklist.ip
WHEN 'email' THEN blacklist.email
WHEN 'phone' THEN blacklist.phone
END AS value,
CASE extra.type
WHEN 'ip' THEN blacklist.ip_id
WHEN 'email' THEN blacklist.email_id
WHEN 'phone' THEN blacklist.phone_id
END AS entity_id
FROM
(
SELECT
event_account.is_important,
event_account.id AS accountid,
event_account.userid AS accounttitle,
event_account.latest_decision AS created,
event_account.score_updated_at,
event_account.score,
account_email.email AS account_email,
CASE WHEN event_ip.fraud_detected THEN split_part(event_ip.ip::text, '/', 1) ELSE NULL END AS ip,
CASE WHEN event_ip.fraud_detected THEN event_ip.id ELSE NULL END AS ip_id,
event_ip.fraud_detected AS ip_fraud,
CASE WHEN event_email.fraud_detected THEN event_email.email ELSE NULL END AS email,
CASE WHEN event_email.fraud_detected THEN event_email.id ELSE NULL END AS email_id,
event_email.fraud_detected AS email_fraud,
CASE WHEN event_phone.fraud_detected THEN event_phone.phone_number ELSE NULL END AS phone,
CASE WHEN event_phone.fraud_detected THEN event_phone.id ELSE NULL END AS phone_id,
event_phone.fraud_detected AS phone_fraud
FROM event
LEFT JOIN event_account
ON event_account.id = event.account
LEFT JOIN event_email AS account_email
ON event_account.lastemail = account_email.id
LEFT JOIN event_ip
ON event_ip.id = event.ip
LEFT JOIN event_email
ON event_email.id = event.email
LEFT JOIN event_phone
ON event_phone.id = event.phone
WHERE
event_account.key = :api_key AND
event_account.fraud IS TRUE AND
(
event_email.fraud_detected IS TRUE OR
event_ip.fraud_detected IS TRUE OR
event_phone.fraud_detected IS TRUE
)
) AS blacklist,
LATERAL (
VALUES
(CASE WHEN ip_fraud = true THEN 'ip' END),
(CASE WHEN email_fraud = true THEN 'email' END),
(CASE WHEN phone_fraud = true THEN 'phone' END)
) AS extra(type)
WHERE
extra.type IS NOT NULL
%s
");
$this->applySearch($query, $queryParams);
$this->applyEntityTypes($query, $queryParams);
$this->applyOrder($query);
$this->applyLimit($query, $queryParams);
return [$query, $queryParams];
}
public function getTotal(): array {
$queryParams = $this->getQueryParams();
$query = ("
SELECT COUNT(*)
FROM (
SELECT DISTINCT
blacklist.accountid,
blacklist.accounttitle,
blacklist.created,
extra.type,
CASE extra.type
WHEN 'ip' THEN blacklist.ip
WHEN 'email' THEN blacklist.email
WHEN 'phone' THEN blacklist.phone
END AS value
FROM
(
SELECT
event_account.id AS accountid,
event_account.userid AS accounttitle,
event_account.latest_decision AS created,
CASE WHEN event_ip.fraud_detected THEN split_part(event_ip.ip::text, '/', 1) ELSE NULL END AS ip,
event_ip.fraud_detected AS ip_fraud,
CASE WHEN event_email.fraud_detected THEN event_email.email ELSE NULL END AS email,
event_email.fraud_detected AS email_fraud,
CASE WHEN event_phone.fraud_detected THEN event_phone.phone_number ELSE NULL END AS phone,
event_phone.fraud_detected AS phone_fraud
FROM event
LEFT JOIN event_account
ON event_account.id = event.account
LEFT JOIN event_ip
ON event_ip.id = event.ip
LEFT JOIN event_email
ON event_email.id = event.email
LEFT JOIN event_phone
ON event_phone.id = event.phone
WHERE
event_account.key = :api_key AND
event_account.fraud IS TRUE AND
(
event_email.fraud_detected IS TRUE OR
event_ip.fraud_detected IS TRUE OR
event_phone.fraud_detected IS TRUE
)
) AS blacklist,
LATERAL (
VALUES
(CASE WHEN ip_fraud = true THEN 'ip' END),
(CASE WHEN email_fraud = true THEN 'email' END),
(CASE WHEN phone_fraud = true THEN 'phone' END)
) AS extra(type)
WHERE
extra.type IS NOT NULL
%s
) AS tbl
");
$this->applySearch($query, $queryParams);
$this->applyEntityTypes($query, $queryParams);
return [$query, $queryParams];
}
private function applySearch(string &$query, array &$queryParams): void {
$this->applyDateRange($query, $queryParams);
$searchConditions = '';
$search = $this->f3->get('REQUEST.search');
if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') {
$searchConditions .= (
" AND (
LOWER(blacklist.accounttitle) LIKE LOWER(:search_value) OR
LOWER(extra.type) LIKE LOWER(:search_value) OR
LOWER(CASE extra.type
WHEN 'ip' THEN blacklist.ip
WHEN 'email' THEN blacklist.email
WHEN 'phone' THEN blacklist.phone
END) LIKE LOWER(:search_value) OR
TO_CHAR(blacklist.created::timestamp without time zone, 'dd/mm/yyyy hh24:mi:ss') LIKE :search_value
)"
);
$queryParams[':search_value'] = '%' . $search['value'] . '%';
}
//Add search into request
$query = sprintf($query, $searchConditions . ' %s');
}
private function applyEntityTypes(string &$query, array &$queryParams): void {
$searchCondition = '';
$entityTypeIds = $this->f3->get('REQUEST.entityTypeIds');
if ($entityTypeIds !== null && count($entityTypeIds)) {
$clauses = [];
foreach ($entityTypeIds as $key => $entityTypeId) {
$clauses[] = 'extra.type = :entity_type_' . $key;
$queryParams[':entity_type_' . $key] = strtolower(\Utils\Constants::get('ENTITY_TYPES')[$entityTypeId]);
}
$searchCondition = ' AND (' . implode(' OR ', $clauses) . ')';
}
$query = sprintf($query, $searchCondition);
}
}

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)
*/
namespace Models\Grid\Bots;
class Grid extends \Models\Grid\Base\Grid {
use \Traits\Enrichment\Devices;
public function __construct(int $apiKey) {
parent::__construct();
$this->apiKey = $apiKey;
$this->idsModel = new Ids($apiKey);
$this->query = new Query($apiKey);
}
public function getAllBots(): array {
return $this->getGrid();
}
protected function calculateCustomParams(array &$result): void {
$this->applyDeviceParams($result);
}
protected function convertTimeToUserTimezone(array &$result): void {
$fields = ['lastseen'];
$this->translateTimeZones($result, $fields);
}
}

View File

@@ -0,0 +1,19 @@
<?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 Models\Grid\Bots;
class Ids extends \Models\Grid\Base\Ids {
}

View File

@@ -0,0 +1,119 @@
<?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 Models\Grid\Bots;
class Query extends \Models\Grid\Base\Query {
protected $defaultOrder = 'ed.lastseen DESC';
protected $dateRangeField = 'ed.lastseen';
protected $allowedColumns = ['id', 'device', 'os_name', 'modified'];
public function getData(): array {
$queryParams = $this->getQueryParams();
$query = (
'SELECT
event_ua_parsed.id,
event_ua_parsed.device,
event_ua_parsed.browser_name,
event_ua_parsed.browser_version,
event_ua_parsed.os_name,
event_ua_parsed.os_version,
event_ua_parsed.ua,
event_ua_parsed.modified,
ed.lastseen
FROM
event_ua_parsed
LEFT JOIN (
SELECT
user_agent,
MAX(lastseen) AS lastseen
FROM event_device
WHERE key = :api_key
GROUP BY user_agent
) AS ed
ON event_ua_parsed.id = ed.user_agent
WHERE
event_ua_parsed.key = :api_key AND
event_ua_parsed.modified IS TRUE
%s'
);
$this->applySearch($query, $queryParams);
$this->applyOrder($query);
$this->applyLimit($query, $queryParams);
return [$query, $queryParams];
}
public function getTotal(): array {
$queryParams = $this->getQueryParams();
$query = (
'SELECT
COUNT(*)
FROM
event_ua_parsed
LEFT JOIN (
SELECT
user_agent,
MAX(lastseen) AS lastseen
FROM event_device
WHERE key = :api_key
GROUP BY user_agent
) AS ed
ON event_ua_parsed.id = ed.user_agent
WHERE
event_ua_parsed.key = :api_key AND
event_ua_parsed.modified IS TRUE
%s'
);
$this->applySearch($query, $queryParams);
return [$query, $queryParams];
}
private function applySearch(string &$query, array &$queryParams): void {
$this->applyDateRange($query, $queryParams);
$searchConditions = '';
$search = $this->f3->get('REQUEST.search');
if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') {
$searchConditions = (
' AND
(
event_ua_parsed.device LIKE LOWER(:search_value) OR
event_ua_parsed.browser_name LIKE LOWER(:search_value) OR
event_ua_parsed.os_name LIKE LOWER(:search_value) OR
event_ua_parsed.ua LIKE LOWER(:search_value)
)'
);
$queryParams[':search_value'] = '%' . $search['value'] . '%';
}
//Add search into request
$query = sprintf($query, $searchConditions);
}
}

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)
*/
namespace Models\Grid\Countries;
class Grid extends \Models\Grid\Base\Grid {
public function __construct(int $apiKey) {
parent::__construct();
$this->apiKey = $apiKey;
$this->idsModel = new Ids($apiKey);
$this->query = new Query($apiKey);
}
public function getAllCountries(): array {
return $this->getGrid();
}
}

View File

@@ -0,0 +1,19 @@
<?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 Models\Grid\Countries;
class Ids extends \Models\Grid\Base\Ids {
}

View File

@@ -0,0 +1,100 @@
<?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 Models\Grid\Countries;
class Query extends \Models\Grid\Base\Query {
protected $defaultOrder = null;
protected $dateRangeField = 'event_country.lastseen';
protected $allowedColumns = ['full_country', 'country', 'total_account', 'total_visit', 'total_ip', 'id'];
public function getData(): array {
$queryParams = $this->getQueryParams();
$query = (
'SELECT
countries.iso AS country_iso,
countries.id AS country_id,
countries.value AS full_country,
countries.id,
event_country.total_visit,
event_country.total_account,
event_country.total_ip
FROM
event_country
LEFT JOIN countries
ON event_country.country = countries.id
WHERE
event_country.key = :api_key
%s'
);
$this->applySearch($query, $queryParams);
$this->applyOrder($query);
return [$query, $queryParams];
}
public function getTotal(): array {
$queryParams = $this->getQueryParams();
$query = (
'SELECT
COUNT(event_country.id)
FROM
event_country
INNER JOIN countries
ON event_country.country = countries.id
WHERE
event_country.key = :api_key
%s'
);
$this->applySearch($query, $queryParams);
return [$query, $queryParams];
}
private function applySearch(string &$query, array &$queryParams): void {
//Add dates into request
$this->applyDateRange($query, $queryParams);
$search = $this->f3->get('REQUEST.search');
$searchConditions = '';
if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') {
$searchConditions .= (
' AND
(
LOWER(countries.value) LIKE LOWER(:search_value)
OR LOWER(countries.iso) LIKE LOWER(:search_value)
)'
);
$queryParams[':search_value'] = '%' . $search['value'] . '%';
}
//Add search and ids into request
$query = sprintf($query, $searchConditions);
}
}

View File

@@ -0,0 +1,60 @@
<?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 Models\Grid\Devices;
class Grid extends \Models\Grid\Base\Grid {
use \Traits\Enrichment\Devices;
public function __construct(int $apiKey) {
parent::__construct();
$this->apiKey = $apiKey;
$this->idsModel = new Ids($apiKey);
$this->query = new Query($apiKey);
}
public function getDevicesByIpId(int $ipId): array {
$params = [':ip_id' => $ipId];
return $this->getGrid($this->idsModel->getDevicesIdsByIpId(), $params);
}
public function getDevicesByUserId(int $userId): array {
$params = [':account_id' => $userId];
return $this->getGrid($this->idsModel->getDevicesIdsByUserId(), $params);
}
public function getDevicesByResourceId(int $resourceId): array {
$params = [':resource_id' => $resourceId];
return $this->getGrid($this->idsModel->getDevicesIdsByResourceId(), $params);
}
public function getAllDevices(): array {
return $this->getGrid();
}
protected function calculateCustomParams(array &$result): void {
$this->applyDeviceParams($result);
}
protected function convertTimeToUserTimezone(array &$result): void {
$fields = ['created'];
$this->translateTimeZones($result, $fields);
}
}

View File

@@ -0,0 +1,51 @@
<?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 Models\Grid\Devices;
class Ids extends \Models\Grid\Base\Ids {
public function getDevicesIdsByIpId(): string {
return (
'SELECT DISTINCT
event.device AS itemid
FROM event
WHERE
event.ip = :ip_id AND
event.key = :api_key'
);
}
public function getDevicesIdsByUserId(): string {
return (
'SELECT DISTINCT
event_device.id AS itemid
FROM event_device
WHERE
event_device.account_id = :account_id AND
event_device.key = :api_key'
);
}
public function getDevicesIdsByResourceId(): string {
return (
'SELECT DISTINCT
event.device AS itemid
FROM event
WHERE
event.url = :resource_id AND
event.key = :api_key'
);
}
}

View File

@@ -0,0 +1,83 @@
<?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 Models\Grid\Devices;
class Query extends \Models\Grid\Base\Query {
protected $defaultOrder = 'event_device.created DESC';
protected $dateRangeField = 'event_device.lastseen';
protected $allowedColumns = ['created', 'device', 'os_name', 'browser_name', 'lang', 'modified', 'id'];
public function getData(): array {
$queryParams = $this->getQueryParams();
$query = (
'SELECT
event_device.id,
event_device.lang,
event_device.created,
event_ua_parsed.device,
event_ua_parsed.browser_name,
event_ua_parsed.browser_version,
event_ua_parsed.os_name,
event_ua_parsed.os_version,
event_ua_parsed.ua,
event_ua_parsed.modified
FROM
event_device
LEFT JOIN event_ua_parsed
ON (event_device.user_agent = event_ua_parsed.id)
WHERE
event_device.key = :api_key
%s'
);
$this->applySearch($query, $queryParams);
$this->applyOrder($query);
$this->applyLimit($query, $queryParams);
return [$query, $queryParams];
}
public function getTotal(): array {
$queryParams = $this->getQueryParams();
$query = (
'SELECT
COUNT(DISTINCT event_device.id)
FROM
event_device
WHERE
event_device.key = :api_key
%s'
);
$this->applySearch($query, $queryParams);
return [$query, $queryParams];
}
private function applySearch(string &$query, array &$queryParams): void {
//$search = $this->f3->get('REQUEST.search');
$searchConditions = $this->injectIdQuery('event_device.id', $queryParams);
//Add ids into request
$query = sprintf($query, $searchConditions);
}
}

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)
*/
namespace Models\Grid\Domains;
class Grid extends \Models\Grid\Base\Grid {
public function __construct(int $apiKey) {
parent::__construct();
$this->apiKey = $apiKey;
$this->idsModel = new Ids($apiKey);
$this->query = new Query($apiKey);
}
public function getDomainsBySameIpDomainId(int $domainId): array {
$params = [':domain_id' => $domainId];
return $this->getGrid($this->idsModel->getDomainsIdsBySameIpDomainId(), $params);
}
public function getAllDomains(): array {
return $this->getGrid();
}
}

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)
*/
namespace Models\Grid\Domains;
class Ids extends \Models\Grid\Base\Ids {
public function getDomainsIdsBySameIpDomainId(): string {
return (
'SELECT DISTINCT
event_domain.id AS itemid
FROM event_domain
WHERE
event_domain.key = :api_key
AND event_domain.ip = (
SELECT
ip
FROM event_domain
WHERE
event_domain.key = :api_key
AND event_domain.id = :domain_id
LIMIT 1
)
AND event_domain.id != :domain_id'
);
}
}

View File

@@ -0,0 +1,107 @@
<?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 Models\Grid\Domains;
class Query extends \Models\Grid\Base\Query {
protected $defaultOrder = 'event_domain.id DESC';
protected $dateRangeField = 'event_domain.lastseen';
protected $allowedColumns = ['domain', 'free_email_provider', 'tranco_rank',
'disabled', 'disposable_domains', 'creation_date', 'total_account', 'fraud', 'id'];
public function getData(): array {
$queryParams = $this->getQueryParams();
$query = (
'SELECT
event_domain.id,
event_domain.domain,
event_domain.ip,
event_domain.total_account,
event_domain.total_visit,
event_domain.disposable_domains,
event_domain.creation_date,
event_domain.disabled,
event_domain.free_email_provider,
event_domain.tranco_rank,
(
SELECT COUNT(*)
FROM event_email
WHERE
event_email.domain = event_domain.id AND
event_email.key = :api_key AND
event_email.fraud_detected IS TRUE
) AS fraud
FROM
event_domain
WHERE
event_domain.key = :api_key
%s
GROUP BY
event_domain.id'
);
$this->applySearch($query, $queryParams);
$this->applyOrder($query);
$this->applyLimit($query, $queryParams);
return [$query, $queryParams];
}
public function getTotal(): array {
$queryParams = $this->getQueryParams();
$query = (
'SELECT
COUNT (event_domain.id)
FROM
event_domain
WHERE
event_domain.key = :api_key
%s'
);
$this->applySearch($query, $queryParams);
return [$query, $queryParams];
}
private function applySearch(string &$query, array &$queryParams): void {
$this->applyDateRange($query, $queryParams);
$search = $this->f3->get('REQUEST.search');
$searchConditions = $this->injectIdQuery('event_domain.id', $queryParams);
if (isset($search) && $search['value'] !== null) {
$searchConditions .= (
' AND (
LOWER(event_domain.domain) LIKE LOWER(:search_value)
OR TEXT(event_domain.creation_date) LIKE LOWER(:search_value)
)'
);
$queryParams[':search_value'] = '%' . $search['value'] . '%';
}
//Add search and ids into request
$query = sprintf($query, $searchConditions);
}
}

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)
*/
namespace Models\Grid\Emails;
class Grid extends \Models\Grid\Base\Grid {
use \Traits\Enrichment\Emails;
public function __construct(int $apiKey) {
parent::__construct();
$this->apiKey = $apiKey;
$this->idsModel = new Ids($apiKey);
$this->query = new Query($apiKey);
}
public function getEmailsByUserId(int $userId): array {
$params = [':account_id' => $userId];
return $this->getGrid($this->idsModel->getEmailsIdsByUserId(), $params);
}
protected function calculateCustomParams(array &$result): void {
$this->calculateEmailReputation($result);
}
protected function convertTimeToUserTimezone(array &$result): void {
$fields = ['lastseen'];
$this->translateTimeZones($result, $fields);
}
}

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)
*/
namespace Models\Grid\Emails;
class Ids extends \Models\Grid\Base\Ids {
public function getEmailsIdsByUserId(): string {
return (
'SELECT DISTINCT
event_email.id AS itemid
FROM event_email
WHERE
event_email.key = :api_key AND
event_email.account_id = :account_id'
);
}
}

View File

@@ -0,0 +1,102 @@
<?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 Models\Grid\Emails;
class Query extends \Models\Grid\Base\Query {
protected $defaultOrder = 'event_email.lastseen DESC';
protected $dateRangeField = 'event_email.lastseen';
protected $allowedColumns = ['email', 'reputation', 'free_email_provider', 'data_breach',
'data_breaches', 'disposable_domains', 'blockemails', 'fraud_detected'];
public function getData(): array {
$queryParams = $this->getQueryParams();
$query = (
'SELECT
event_email.id,
event_email.email,
event_email.data_breach,
event_email.data_breaches,
event_email.fraud_detected,
-- event_email.profiles,
event_email.blockemails,
event_email.lastseen,
event_email.alert_list,
event_domain.domain,
event_domain.id AS domain_id,
event_domain.free_email_provider,
event_domain.disposable_domains
FROM
event_email
LEFT JOIN event_domain
ON (event_email.domain = event_domain.id)
WHERE
event_email.key = :api_key
%s'
);
$this->applySearch($query, $queryParams);
$this->applyOrder($query);
$this->applyLimit($query, $queryParams);
return [$query, $queryParams];
}
public function getTotal(): array {
$queryParams = $this->getQueryParams();
$query = (
'SELECT
COUNT(*)
FROM
event_email
WHERE
event_email.key = :api_key
%s'
);
$this->applySearch($query, $queryParams);
return [$query, $queryParams];
}
private function applySearch(string &$query, array &$queryParams): void {
$search = $this->f3->get('REQUEST.search');
$searchConditions = $this->injectIdQuery('event_email.id', $queryParams);
if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') {
$searchConditions .= (
' AND
(
event_email.email LIKE :search_value
OR TEXT(event_email.lastseen) LIKE :search_value
)'
);
$queryParams[':search_value'] = '%' . $search['value'] . '%';
}
//Add search and ids into request
$query = sprintf($query, $searchConditions);
}
}

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 Models\Grid\Events;
class Grid extends \Models\Grid\Base\Grid {
use \Traits\Enrichment\Ips;
use \Traits\Enrichment\Devices;
public function __construct(int $apiKey) {
parent::__construct();
$this->apiKey = $apiKey;
$this->idsModel = new Ids($apiKey);
$this->query = new Query($apiKey);
}
public function getEventsByUserId(int $userId): array {
$ids = ['userId' => $userId];
return $this->getGrid(null, $ids);
}
public function getEventsByIspId(int $ispId): array {
$ids = ['ispId' => $ispId];
return $this->getGrid(null, $ids);
}
public function getEventsByDomainId(int $domainId): array {
$ids = ['domainId' => $domainId];
return $this->getGrid(null, $ids);
}
public function getEventsByDeviceId(int $deviceId): array {
$ids = ['deviceId' => $deviceId];
return $this->getGrid(null, $ids);
}
public function getEventsByResourceId(int $resourceId): array {
$ids = ['resourceId' => $resourceId];
return $this->getGrid(null, $ids);
}
public function getEventsByCountryId(int $countryId): array {
$ids = ['countryId' => $countryId];
return $this->getGrid(null, $ids);
}
public function getEventsByIpId(int $ipId): array {
$ids = ['ipId' => $ipId];
return $this->getGrid(null, $ids);
}
public function getAllEvents() {
return $this->getGrid();
}
protected function calculateCustomParams(array &$result): void {
$this->calculateIpType($result);
$this->applyDeviceParams($result);
}
protected function convertTimeToUserTimezone(array &$result): void {
$fields = ['time', 'lastseen', 'session_max_t', 'session_min_t', 'score_updated_at'];
$this->translateTimeZones($result, $fields);
}
}

View File

@@ -0,0 +1,19 @@
<?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 Models\Grid\Events;
class Ids extends \Models\Grid\Base\Ids {
}

View File

@@ -0,0 +1,365 @@
<?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 Models\Grid\Events;
class Query extends \Models\Grid\Base\Query {
protected $defaultOrder = 'event.time DESC, event.id DESC';
protected $dateRangeField = 'event.time';
protected $allowedColumns = ['userid', 'time', 'type', 'ip', 'ip_type', 'device', 'session_id', 'time', 'id'];
public function getData(): array {
$queryParams = $this->getQueryParams();
$query = (
'SELECT
event.id,
event.time,
event_type.value AS event_type,
event_type.name AS event_type_name,
event_account.is_important,
event_account.id AS accountid,
event_account.userid AS accounttitle,
event_account.score_updated_at,
event_account.score,
event_account.fraud,
event_url.url,
event_url.id as url_id,
event_url_query.query,
event_url.title,
event_ip.ip,
event_ip.data_center,
event_ip.vpn,
event_ip.tor,
event_ip.relay,
event_ip.starlink,
event_ip.blocklist,
event_ip.fraud_detected,
event_ip.checked,
event_isp.name AS isp_name,
countries.iso AS country_iso,
countries.id AS country_id,
countries.value AS full_country,
event_ua_parsed.ua,
event_ua_parsed.device,
event_ua_parsed.os_name,
event_email.email,
event.http_code,
event.session_id,
event_session.total_visit AS session_cnt,
event_session.lastseen AS session_max_t,
event_session.created AS session_min_t,
event_session.lastseen - event_session.created AS session_duration
FROM
event
INNER JOIN event_account
ON (event.account = event_account.id)
INNER JOIN event_url
ON (event.url = event_url.id)
FULL OUTER JOIN event_url_query
ON (event.query = event_url_query.id)
LEFT JOIN event_device
ON (event.device = event_device.id)
LEFT JOIN event_type
ON (event.type = event_type.id)
LEFT JOIN event_ua_parsed
ON (event_device.user_agent = event_ua_parsed.id)
LEFT JOIN event_ip
ON (event.ip = event_ip.id)
LEFT JOIN event_isp
ON (event_ip.isp = event_isp.id)
INNER JOIN countries
ON (event_ip.country = countries.id)
LEFT JOIN event_email
ON (event.email = event_email.id)
LEFT JOIN event_session
ON (event.time = event_session.lastseen AND event.session_id = event_session.id)
WHERE
event.key = :api_key
%s'
);
$this->applySearch($query, $queryParams);
$this->applyEventTypes($query, $queryParams);
$this->applyDeviceTypes($query, $queryParams);
$this->applyRules($query, $queryParams);
$this->applyOrder($query);
$this->applyLimit($query, $queryParams);
return [$query, $queryParams];
}
public function getTotal(): array {
$query = null;
$queryParams = $this->getQueryParams();
if ($this->itemId !== null) {
switch ($this->itemKey) {
case 'userId':
$query = 'SELECT total_visit AS count FROM event_account WHERE key = :api_key AND id = :item_id';
break;
case 'ispId':
$query = 'SELECT total_visit AS count FROM event_isp WHERE key = :api_key AND id = :item_id';
break;
case 'domainId':
$query = 'SELECT total_visit AS count FROM event_domain WHERE key = :api_key AND id = :item_id';
break;
case 'resourceId':
$query = 'SELECT total_visit AS count FROM event_url WHERE key = :api_key AND id = :item_id';
break;
case 'countryId':
$query = 'SELECT total_visit AS count FROM event_country WHERE key = :api_key AND country = :item_id';
break;
case 'ipId':
$query = 'SELECT total_visit AS count FROM event_ip WHERE key = :api_key AND id = :item_id';
break;
case 'deviceId':
$query = (
'SELECT
COUNT(event.id) AS count
FROM event
INNER JOIN event_device
ON (event.device = event_device.id)
WHERE
event_device.key = :api_key AND
event_device.user_agent = :item_id'
);
break;
}
}
if (!$query) {
$query = (
'SELECT
COUNT(event.id) AS count
FROM
event
INNER JOIN event_account
ON (event.account = event_account.id)
INNER JOIN event_url
ON (event.url = event_url.id)
INNER JOIN event_ip
ON (event.ip = event_ip.id)
INNER JOIN countries
ON (event_ip.country = countries.id)
LEFT JOIN event_email
ON (event.email = event_email.id)
LEFT JOIN event_device
ON (event.device = event_device.id)
LEFT JOIN event_type
ON (event.type = event_type.id)
LEFT JOIN event_ua_parsed
ON (event_device.user_agent = event_ua_parsed.id)
WHERE
event.key = :api_key
%s'
);
$this->applySearch($query, $queryParams);
$this->applyEventTypes($query, $queryParams);
$this->applyDeviceTypes($query, $queryParams);
$this->applyRules($query, $queryParams);
}
return [$query, $queryParams];
}
private function applySearch(string &$query, array &$queryParams): void {
//Add dates into request
$this->applyDateRange($query, $queryParams);
//Apply itemId into request
$this->applyRelatedToIdSearchConitions($query);
$searchConditions = '';
$search = $this->f3->get('REQUEST.search');
if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') {
//TODO: user isIp function here
if (filter_var($search['value'], FILTER_VALIDATE_IP) !== false) {
$searchConditions .= (
' AND
(
event_ip.ip = :search_value
)'
);
$queryParams[':search_value'] = $search['value'];
} else {
// https://stackoverflow.com/a/63701098
$searchConditions .= (
" AND
(
LOWER(event_email.email) LIKE LOWER(:search_value) OR
LOWER(event_account.userid) LIKE LOWER(:search_value) OR
event.http_code::text LIKE LOWER(:search_value) OR
CASE WHEN event.http_code >= 400 THEN
CONCAT('error ', event.http_code)
ELSE
'' END LIKE LOWER(:search_value) OR
TO_CHAR(event.time::timestamp without time zone, 'dd/mm/yyyy hh24:mi:ss') LIKE :search_value
)"
);
$queryParams[':search_value'] = '%' . $search['value'] . '%';
}
}
//Add search and ids into request
$query = sprintf($query, $searchConditions);
}
protected function getQueryParams(): array {
$params = [':api_key' => $this->apiKey];
if ($this->itemId !== null) {
$params[':item_id'] = $this->itemId;
}
return $params;
}
private function applyRelatedToIdSearchConitions(string &$query): void {
$searchConditions = null;
if ($this->itemId !== null) {
switch ($this->itemKey) {
case 'userId':
$searchConditions = ' AND event.account = :item_id %s';
break;
case 'ispId':
$searchConditions = ' AND event_isp.id = :item_id %s';
break;
case 'domainId':
$searchConditions = ' AND event_email.domain = :item_id %s';
break;
case 'resourceId':
$searchConditions = ' AND event.url = :item_id %s';
break;
case 'countryId':
$searchConditions = ' AND countries.id = :item_id %s';
break;
case 'ipId':
$searchConditions = ' AND event_ip.id = :item_id %s';
break;
case 'deviceId':
$searchConditions = ' AND event_ua_parsed.id = :item_id %s';
break;
}
}
//Add search and ids into request
if ($searchConditions !== null) {
$query = sprintf($query, $searchConditions);
}
}
private function applyEventTypes(string &$query, array &$queryParams): void {
$eventTypeIds = $this->f3->get('REQUEST.eventTypeIds');
if ($eventTypeIds === null || !count($eventTypeIds)) {
return;
}
$clauses = [];
foreach ($eventTypeIds as $key => $eventTypeId) {
$clauses[] = 'event.type = :event_type_id_' . $key;
$queryParams[':event_type_id_' . $key] = $eventTypeId;
}
$query .= ' AND (' . implode(' OR ', $clauses) . ')';
}
private function applyDeviceTypes(string &$query, array &$queryParams): void {
$deviceTypes = $this->f3->get('REQUEST.deviceTypes');
if ($deviceTypes === null || !count($deviceTypes)) {
return;
}
$clauses = [];
foreach ($deviceTypes as $key => $deviceType) {
if ($deviceType === 'other') {
$placeholders = [];
foreach (\Utils\Constants::get('DEVICE_TYPES') as $device) {
if ($device !== 'unknown' && $device !== 'other') {
$param = ':device_exclude_' . $device;
$placeholders[] = $param;
$queryParams[$param] = $device;
}
}
$params = implode(', ', $placeholders);
$clauses[] = '(event_ua_parsed.device NOT IN (' . $params . ') AND event_ua_parsed.device IS NOT NULL)';
} elseif ($deviceType === 'unknown') {
$clauses[] = 'event_ua_parsed.device IS NULL';
} else {
$param = ':device_' . $key;
$clauses[] = 'event_ua_parsed.device = ' . $param;
$queryParams[$param] = $deviceType;
}
}
$query .= ' AND (' . implode(' OR ', $clauses) . ')';
}
private function applyRules(string &$query, array &$queryParams): void {
$ruleUids = $this->f3->get('REQUEST.ruleUids');
if ($ruleUids === null) {
return;
}
$uids = [];
foreach ($ruleUids as $ruleUid) {
$uids[] = ['uid' => $ruleUid];
}
$query .= ' AND score_details @> :rules_uids::jsonb';
$queryParams[':rules_uids'] = json_encode($uids);
}
}

View File

@@ -0,0 +1,72 @@
<?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 Models\Grid\Ips;
class Grid extends \Models\Grid\Base\Grid {
use \Traits\Enrichment\Ips;
public function __construct(int $apiKey) {
parent::__construct();
$this->apiKey = $apiKey;
$this->idsModel = new Ids($apiKey);
$this->query = new Query($apiKey);
}
public function getIpsByUserId(int $userId): array {
$params = [':account_id' => $userId];
return $this->getGrid($this->idsModel->getIpsIdsByUserId(), $params);
}
public function getIpsByIspId(int $ispId): array {
$params = [':isp_id' => $ispId];
return $this->getGrid($this->idsModel->getIpsIdsByIspId(), $params);
}
public function getIpsByDomainId($domainId) {
$params = [':domain_id' => $domainId];
return $this->getGrid($this->idsModel->getIpsIdsByDomainId(), $params);
}
public function getIpsByCountryId(int $countryId): array {
$params = [':country_id' => $countryId];
return $this->getGrid($this->idsModel->getIpsIdsByCountryId(), $params);
}
public function getIpsByDeviceId(int $deviceId): array {
$params = [':device_id' => $deviceId];
return $this->getGrid($this->idsModel->getIpsIdsByDeviceId(), $params);
}
public function getIpsByResourceId(int $resourceId): array {
$params = [':resource_id' => $resourceId];
return $this->getGrid($this->idsModel->getIpsIdsByResourceId(), $params);
}
public function getAllIps() {
return $this->getGrid();
}
protected function calculateCustomParams(array &$result): void {
$this->calculateIpType($result);
}
}

View File

@@ -0,0 +1,88 @@
<?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 Models\Grid\Ips;
class Ids extends \Models\Grid\Base\Ids {
public function getIpsIdsByUserId(): string {
return (
'SELECT DISTINCT
event.ip AS itemid
FROM event
WHERE
event.key = :api_key
AND event.account = :account_id'
);
}
public function getIpsIdsByIspId(): string {
return (
'SELECT DISTINCT
event_ip.id AS itemid
FROM event_ip
WHERE
event_ip.key = :api_key
AND event_ip.isp = :isp_id'
);
}
public function getIpsIdsByDomainId(): string {
return (
'SELECT DISTINCT
event.ip AS itemid
FROM event
LEFT JOIN event_email
ON (event.email = event_email.id)
WHERE
event_email.key = :api_key
AND event_email.domain = :domain_id'
);
}
public function getIpsIdsByCountryId(): string {
return (
'SELECT DISTINCT
event_ip.id AS itemid
FROM event_ip
WHERE
event_ip.key = :api_key AND
event_ip.country = :country_id'
);
}
public function getIpsIdsByDeviceId(): string {
return (
'SELECT DISTINCT
event.ip AS itemid
FROM event
INNER JOIN event_device
ON (event.device = event_device.id)
WHERE
event_device.user_agent = :device_id AND
event_device.key = :api_key'
);
}
public function getIpsIdsByResourceId(): string {
return (
'SELECT DISTINCT
event.ip AS itemid
FROM event
WHERE
event.url = :resource_id AND
event.key = :api_key'
);
}
}

View File

@@ -0,0 +1,168 @@
<?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 Models\Grid\Ips;
class Query extends \Models\Grid\Base\Query {
protected $defaultOrder = 'event_ip.lastseen DESC';
protected $dateRangeField = 'event_ip.lastseen';
protected $allowedColumns = ['ip', 'full_country', 'asn', 'netname', 'ip_type', 'total_visit', 'total_account', 'lastseen', 'id'];
public function getData(): array {
$queryParams = $this->getQueryParams();
$query = (
'SELECT
event_ip.id,
event_ip.ip,
event_ip.fraud_detected,
event_ip.alert_list,
event_ip.data_center,
event_ip.vpn,
event_ip.tor,
event_ip.relay,
event_ip.blocklist,
event_ip.starlink,
event_ip.shared AS total_account,
event_ip.total_visit,
event_ip.checked,
event_ip.lastseen AS lastseen,
event_isp.name AS netname,
event_isp.description,
event_isp.asn,
countries.id AS country_id,
countries.iso AS country_iso,
countries.value AS full_country
FROM
event_ip
LEFT JOIN countries
ON (event_ip.country = countries.id)
LEFT JOIN event_isp
ON (event_ip.isp = event_isp.id)
WHERE
event_ip.key = :api_key
%s'
);
$this->applySearch($query, $queryParams);
$this->applyIpTypes($query);
$this->applyOrder($query);
$this->applyLimit($query, $queryParams);
return [$query, $queryParams];
}
public function getTotal(): array {
$queryParams = $this->getQueryParams();
$query = (
'SELECT
COUNT (DISTINCT event_ip.ip)
FROM
event_ip
LEFT JOIN countries
ON (event_ip.country = countries.id)
LEFT JOIN event_isp
ON (event_ip.isp = event_isp.id)
WHERE
event_ip.key = :api_key
%s'
);
$this->applySearch($query, $queryParams);
$this->applyIpTypes($query);
return [$query, $queryParams];
}
private function applySearch(string &$query, array &$queryParams): void {
$this->applyDateRange($query, $queryParams);
$search = $this->f3->get('REQUEST.search');
$searchConditions = $this->injectIdQuery('event_ip.id', $queryParams);
if (is_array($search) && isset($search['value']) && is_string($search['value']) && $search['value'] !== '') {
$searchConditions .= (
' AND
(
TEXT(event_ip.ip) LIKE LOWER(:search_value)
OR LOWER(event_isp.asn::text) LIKE LOWER(:search_value)
OR LOWER(event_isp.name) LIKE LOWER(:search_value)
OR LOWER(countries.value) LIKE LOWER(:search_value)
OR LOWER(countries.iso) LIKE LOWER(:search_value)
)'
);
$queryParams[':search_value'] = '%' . $search['value'] . '%';
}
//Add search and ids into request
$query = sprintf($query, $searchConditions);
}
private function applyIpTypes(string &$query): void {
$ipTypeIds = $this->f3->get('REQUEST.ipTypeIds');
if ($ipTypeIds === null) {
return;
}
foreach ($ipTypeIds as $ipTypeId) {
switch ($ipTypeId) {
case 0:
$query .= ' AND fraud_detected IS TRUE ';
break;
case 1:
$query .= ' AND blocklist IS TRUE ';
break;
case 2:
$query .= ' AND countries.id = 0 AND event_ip.checked IS TRUE ';
break;
case 3:
$query .= ' AND tor IS TRUE ';
break;
case 4:
$query .= ' AND starlink IS TRUE ';
break;
case 5:
$query .= ' AND relay IS TRUE ';
break;
case 6:
$query .= ' AND vpn IS TRUE ';
break;
case 7:
$query .= ' AND data_center IS TRUE ';
break;
case 8:
$query .= ' AND (event_ip.checked IS FALSE OR event_ip.checked IS NULL) ';
break;
case 9:
$query .= ' AND (tor IS FALSE AND vpn IS FALSE AND relay IS FALSE AND data_center IS FALSE AND event_ip.checked IS TRUE) ';
break;
}
}
}
}

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